diff --git a/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml b/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml index db94e74f..5f872338 100644 --- a/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml +++ b/.github/workflows/azure-static-web-apps-mango-bush-01e607f10.yml @@ -4,10 +4,6 @@ on: push: branches: - main - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - main jobs: build_and_deploy_job: @@ -25,13 +21,13 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.27.3' # Specify the Flutter version you want to use + flutter-version: '3.32.1' # Specify the Flutter version you want to use - name: Install dependencies run: flutter pub get - name: Build Flutter Web App - run: flutter build web --web-renderer canvaskit -t lib/main.dart + run: flutter build web --release -t lib/main.dart - name: Build And Deploy id: builddeploy diff --git a/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml b/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml index 738bd279..0721eeec 100644 --- a/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml +++ b/.github/workflows/azure-static-web-apps-polite-smoke-017c65c10.yml @@ -4,18 +4,12 @@ on: push: branches: - dev - pull_request: - types: [opened, synchronize, reopened, closed] - branches: - - dev jobs: build_and_deploy_job: - if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.action != 'closed') runs-on: ubuntu-latest name: Build and Deploy Job steps: - - name: Checkout Code uses: actions/checkout@v3 with: @@ -25,13 +19,13 @@ jobs: - name: Set up Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.27.3' # Specify the Flutter version you want to use + flutter-version: '3.32.1' # Specify the Flutter version you want to use - name: Install dependencies run: flutter pub get - name: Build Flutter Web App - run: flutter build web --web-renderer canvaskit -t lib/main_dev.dart + run: flutter build web --release -t lib/main_dev.dart - name: Build And Deploy id: builddeploy diff --git a/.github/workflows/pr-check.yml b/.github/workflows/pr-check.yml new file mode 100644 index 00000000..f6ef8c91 --- /dev/null +++ b/.github/workflows/pr-check.yml @@ -0,0 +1,29 @@ +name: Pull Request Check + +on: + pull_request: + branches: + - dev + - main + +jobs: + setup_flutter: + runs-on: ubuntu-latest + name: Setup Flutter and Dependencies + steps: + - name: Checkout Code + uses: actions/checkout@v3 + with: + submodules: true + lfs: false + + - name: Set up Flutter + uses: subosito/flutter-action@v2 + with: + flutter-version: '3.32.1' + + - name: Install dependencies + run: flutter pub get + + - name: Run Flutter Build + run: flutter build web --release -t lib/main_dev.dart diff --git a/.gitignore b/.gitignore index 29a3a501..fa9d8443 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ migrate_working_dir/ .pub-cache/ .pub/ /build/ +pubspec.lock # Symbolication related app.*.symbols diff --git a/.vscode/launch.json b/.vscode/launch.json index f81a9deb..4aceb26d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -16,6 +16,7 @@ "3000", "-t", "lib/main_dev.dart", + "--web-experimental-hot-reload", ], "flutterMode": "debug" @@ -35,6 +36,7 @@ "3000", "-t", "lib/main_staging.dart", + "--web-experimental-hot-reload", ], "flutterMode": "debug" @@ -54,6 +56,7 @@ "3000", "-t", "lib/main.dart", + "--web-experimental-hot-reload", ], "flutterMode": "debug" diff --git a/analysis_options.yaml b/analysis_options.yaml index 81bdd00d..cfae8a7e 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -1,32 +1,32 @@ -# 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`. +include: package:very_good_analysis/analysis_options.yaml -# The following line activates a set of recommended lints for Flutter apps, -# packages, and plugins designed to encourage good coding practices. analyzer: errors: - constant_identifier_names: ignore -include: package:flutter_lints/flutter.yaml + strict_raw_type: warning + argument_type_not_assignable: warning + invalid_assignment: warning + return_of_invalid_type: warning + return_of_invalid_type_from_closure: warning + list_element_type_not_assignable: warning + for_in_of_invalid_type: warning + cast_nullable_to_non_nullable: warning + non_bool_condition: warning + field_initializer_not_assignable: warning + non_bool_negation_expression: warning + non_bool_operand: warning linter: - # The lint rules applied to this project can be customized in the - # section below to disable rules from the `package:flutter_lints/flutter.yaml` - # included above or to enable additional rules. A list of all available lints - # and their documentation is published at https://dart.dev/lints. - # - # Instead of disabling a lint rule for the entire project in the - # section below, it can also be suppressed for a single line of code - # or a specific dart file by using the `// ignore: name_of_lint` and - # `// ignore_for_file: name_of_lint` syntax on the line or in the file - # producing the lint. rules: - # avoid_print: false # Uncomment to disable the `avoid_print` rule - # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule - prefer_const_constructors: true - -# Additional information about this file can be found at -# https://dart.dev/guides/language/analysis-options + prefer_single_quotes: true + avoid_print: false + public_member_api_docs: false + sort_pub_dependencies: false + one_member_abstracts: false + prefer_int_literals: false + sort_constructors_first: false + avoid_redundant_argument_values: false + always_put_required_named_parameters_first: false + unnecessary_breaks: false + avoid_catches_without_on_clauses: false + cascade_invocations: false + overridden_fields: false diff --git a/android/.gitignore b/android/.gitignore deleted file mode 100644 index 6f568019..00000000 --- a/android/.gitignore +++ /dev/null @@ -1,13 +0,0 @@ -gradle-wrapper.jar -/.gradle -/captures/ -/gradlew -/gradlew.bat -/local.properties -GeneratedPluginRegistrant.java - -# Remember to never publicly share your keystore. -# See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app -key.properties -**/*.keystore -**/*.jks diff --git a/android/app/build.gradle b/android/app/build.gradle deleted file mode 100644 index 8981dae5..00000000 --- a/android/app/build.gradle +++ /dev/null @@ -1,71 +0,0 @@ -plugins { - id "com.android.application" - // START: FlutterFire Configuration - id 'com.google.gms.google-services' - id 'com.google.firebase.crashlytics' - // END: FlutterFire Configuration - id "kotlin-android" - id "dev.flutter.flutter-gradle-plugin" -} - -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -android { - namespace "com.example.syncrow_web" - compileSdk flutter.compileSdkVersion - ndkVersion flutter.ndkVersion - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } - - kotlinOptions { - jvmTarget = '1.8' - } - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "com.example.syncrow_web" - // 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 flutter.minSdkVersion - targetSdkVersion flutter.targetSdkVersion - 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 {} diff --git a/android/app/google-services.json b/android/app/google-services.json deleted file mode 100644 index 3cc77ccd..00000000 --- a/android/app/google-services.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "project_info": { - "project_number": "427332280600", - "firebase_url": "https://test2-8a3d2-default-rtdb.firebaseio.com", - "project_id": "test2-8a3d2", - "storage_bucket": "test2-8a3d2.firebasestorage.app" - }, - "client": [ - { - "client_info": { - "mobilesdk_app_id": "1:427332280600:android:550f67441246cb1a0c7e6d", - "android_client_info": { - "package_name": "com.example.syncrow.app" - } - }, - "oauth_client": [], - "api_key": [ - { - "current_key": "AIzaSyA5qOErxdm0zJmoHIB0TixfebYEsNRpwV0" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [] - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:427332280600:android:bb6047adeeb80fb00c7e6d", - "android_client_info": { - "package_name": "com.example.syncrow_application" - } - }, - "oauth_client": [], - "api_key": [ - { - "current_key": "AIzaSyA5qOErxdm0zJmoHIB0TixfebYEsNRpwV0" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [] - } - } - }, - { - "client_info": { - "mobilesdk_app_id": "1:427332280600:android:2bc36fbe82994a3e0c7e6d", - "android_client_info": { - "package_name": "com.example.syncrow_web" - } - }, - "oauth_client": [], - "api_key": [ - { - "current_key": "AIzaSyA5qOErxdm0zJmoHIB0TixfebYEsNRpwV0" - } - ], - "services": { - "appinvite_service": { - "other_platform_oauth_client": [] - } - } - } - ], - "configuration_version": "1" -} \ No newline at end of file diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml deleted file mode 100644 index 399f6981..00000000 --- a/android/app/src/debug/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml deleted file mode 100644 index efddc5a1..00000000 --- a/android/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,44 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/android/app/src/main/kotlin/com/example/syncrow_web/MainActivity.kt b/android/app/src/main/kotlin/com/example/syncrow_web/MainActivity.kt deleted file mode 100644 index 85468d7a..00000000 --- a/android/app/src/main/kotlin/com/example/syncrow_web/MainActivity.kt +++ /dev/null @@ -1,5 +0,0 @@ -package com.example.syncrow_web - -import io.flutter.embedding.android.FlutterActivity - -class MainActivity: FlutterActivity() diff --git a/android/app/src/main/res/drawable-v21/launch_background.xml b/android/app/src/main/res/drawable-v21/launch_background.xml deleted file mode 100644 index f74085f3..00000000 --- a/android/app/src/main/res/drawable-v21/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml deleted file mode 100644 index 304732f8..00000000 --- a/android/app/src/main/res/drawable/launch_background.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png deleted file mode 100644 index db77bb4b..00000000 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png deleted file mode 100644 index 17987b79..00000000 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png deleted file mode 100644 index 09d43914..00000000 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png deleted file mode 100644 index d5f1c8d3..00000000 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png deleted file mode 100644 index 4d6372ee..00000000 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and /dev/null differ diff --git a/android/app/src/main/res/values-night/styles.xml b/android/app/src/main/res/values-night/styles.xml deleted file mode 100644 index 06952be7..00000000 --- a/android/app/src/main/res/values-night/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml deleted file mode 100644 index cb1ef880..00000000 --- a/android/app/src/main/res/values/styles.xml +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - diff --git a/android/app/src/profile/AndroidManifest.xml b/android/app/src/profile/AndroidManifest.xml deleted file mode 100644 index 399f6981..00000000 --- a/android/app/src/profile/AndroidManifest.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/android/build.gradle b/android/build.gradle deleted file mode 100644 index bc157bd1..00000000 --- a/android/build.gradle +++ /dev/null @@ -1,18 +0,0 @@ -allprojects { - repositories { - google() - mavenCentral() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -tasks.register("clean", Delete) { - delete rootProject.buildDir -} diff --git a/android/gradle.properties b/android/gradle.properties deleted file mode 100644 index 598d13fe..00000000 --- a/android/gradle.properties +++ /dev/null @@ -1,3 +0,0 @@ -org.gradle.jvmargs=-Xmx4G -android.useAndroidX=true -android.enableJetifier=true diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index e1ca574e..00000000 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,5 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.3-all.zip diff --git a/android/settings.gradle b/android/settings.gradle deleted file mode 100644 index 85edcfc9..00000000 --- a/android/settings.gradle +++ /dev/null @@ -1,30 +0,0 @@ -pluginManagement { - def flutterSdkPath = { - def properties = new Properties() - file("local.properties").withInputStream { properties.load(it) } - def flutterSdkPath = properties.getProperty("flutter.sdk") - assert flutterSdkPath != null, "flutter.sdk not set in local.properties" - return flutterSdkPath - } - settings.ext.flutterSdkPath = flutterSdkPath() - - includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") - - repositories { - google() - mavenCentral() - gradlePluginPortal() - } -} - -plugins { - id "dev.flutter.flutter-plugin-loader" version "1.0.0" - id "com.android.application" version "7.3.0" apply false - // START: FlutterFire Configuration - id "com.google.gms.google-services" version "4.3.15" apply false - id "com.google.firebase.crashlytics" version "2.8.1" apply false - // END: FlutterFire Configuration - id "org.jetbrains.kotlin.android" version "1.7.10" apply false -} - -include ":app" diff --git a/assets/icons/aqi_air_quality.svg b/assets/icons/aqi_air_quality.svg new file mode 100644 index 00000000..cd2ed556 --- /dev/null +++ b/assets/icons/aqi_air_quality.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/icons/aqi_humidity.svg b/assets/icons/aqi_humidity.svg new file mode 100644 index 00000000..dd8a1d7e --- /dev/null +++ b/assets/icons/aqi_humidity.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/aqi_temperature.svg b/assets/icons/aqi_temperature.svg new file mode 100644 index 00000000..09ab6d77 --- /dev/null +++ b/assets/icons/aqi_temperature.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/icons/close_settings_icon.svg b/assets/icons/close_settings_icon.svg new file mode 100644 index 00000000..93e615d8 --- /dev/null +++ b/assets/icons/close_settings_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/edit_name_icon_settings.svg b/assets/icons/edit_name_icon_settings.svg new file mode 100644 index 00000000..54bee0af --- /dev/null +++ b/assets/icons/edit_name_icon_settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/energy_consumed_icon.svg b/assets/icons/energy_consumed_icon.svg new file mode 100644 index 00000000..d457619c --- /dev/null +++ b/assets/icons/energy_consumed_icon.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/assets/icons/humidity.svg b/assets/icons/humidity.svg new file mode 100644 index 00000000..585ac31f --- /dev/null +++ b/assets/icons/humidity.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/assets/icons/landing_analytics.svg b/assets/icons/landing_analytics.svg new file mode 100644 index 00000000..6f9fbbf0 --- /dev/null +++ b/assets/icons/landing_analytics.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/location_pin.svg b/assets/icons/location_pin.svg new file mode 100644 index 00000000..e1ae063f --- /dev/null +++ b/assets/icons/location_pin.svg @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/assets/icons/refresh_status_icon.svg b/assets/icons/refresh_status_icon.svg new file mode 100644 index 00000000..eb375bd8 --- /dev/null +++ b/assets/icons/refresh_status_icon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/assets/icons/sittings_button.svg b/assets/icons/sittings_button.svg new file mode 100644 index 00000000..43cad368 --- /dev/null +++ b/assets/icons/sittings_button.svg @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/icons/thermometer.svg b/assets/icons/thermometer.svg new file mode 100644 index 00000000..94aa72eb --- /dev/null +++ b/assets/icons/thermometer.svg @@ -0,0 +1,8 @@ + + + + + + + + diff --git a/assets/images/web_Background.png b/assets/images/web_Background.png new file mode 100644 index 00000000..1d1dac6e Binary files /dev/null and b/assets/images/web_Background.png differ diff --git a/ios/.gitignore b/ios/.gitignore deleted file mode 100644 index 7a7f9873..00000000 --- a/ios/.gitignore +++ /dev/null @@ -1,34 +0,0 @@ -**/dgph -*.mode1v3 -*.mode2v3 -*.moved-aside -*.pbxuser -*.perspectivev3 -**/*sync/ -.sconsign.dblite -.tags* -**/.vagrant/ -**/DerivedData/ -Icon? -**/Pods/ -**/.symlinks/ -profile -xcuserdata -**/.generated/ -Flutter/App.framework -Flutter/Flutter.framework -Flutter/Flutter.podspec -Flutter/Generated.xcconfig -Flutter/ephemeral/ -Flutter/app.flx -Flutter/app.zip -Flutter/flutter_assets/ -Flutter/flutter_export_environment.sh -ServiceDefinitions.json -Runner/GeneratedPluginRegistrant.* - -# Exceptions to above rules. -!default.mode1v3 -!default.mode2v3 -!default.pbxuser -!default.perspectivev3 diff --git a/ios/Flutter/AppFrameworkInfo.plist b/ios/Flutter/AppFrameworkInfo.plist deleted file mode 100644 index 7c569640..00000000 --- a/ios/Flutter/AppFrameworkInfo.plist +++ /dev/null @@ -1,26 +0,0 @@ - - - - - CFBundleDevelopmentRegion - en - CFBundleExecutable - App - CFBundleIdentifier - io.flutter.flutter.app - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - App - CFBundlePackageType - FMWK - CFBundleShortVersionString - 1.0 - CFBundleSignature - ???? - CFBundleVersion - 1.0 - MinimumOSVersion - 12.0 - - diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig deleted file mode 100644 index ec97fc6f..00000000 --- a/ios/Flutter/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig deleted file mode 100644 index c4855bfe..00000000 --- a/ios/Flutter/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile deleted file mode 100644 index d97f17e2..00000000 --- a/ios/Podfile +++ /dev/null @@ -1,44 +0,0 @@ -# Uncomment this line to define a global platform for your project -# platform :ios, '12.0' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_ios_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_ios_build_settings(target) - end -end diff --git a/ios/Podfile.lock b/ios/Podfile.lock deleted file mode 100644 index 85fd7a99..00000000 --- a/ios/Podfile.lock +++ /dev/null @@ -1,36 +0,0 @@ -PODS: - - Flutter (1.0.0) - - flutter_secure_storage (6.0.0): - - Flutter - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - -DEPENDENCIES: - - Flutter (from `Flutter`) - - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - -EXTERNAL SOURCES: - Flutter: - :path: Flutter - flutter_secure_storage: - :path: ".symlinks/plugins/flutter_secure_storage/ios" - path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/darwin" - shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/darwin" - -SPEC CHECKSUMS: - Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 - flutter_secure_storage: d33dac7ae2ea08509be337e775f6b59f1ff45f12 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - -PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 - -COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 4f245401..00000000 --- a/ios/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,751 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXBuildFile section */ - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; - 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 */; }; - E44A9405B1EB1B638DD05A58 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 7ABF0EC746A2D686A0ED574F /* Pods_RunnerTests.framework */; }; - F2A3345EC3021060731668D3 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = B14AB50E8716720E10D074BD /* GoogleService-Info.plist */; }; - FF49F60EC38658783D8D66DA /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 2AFAE479A87ECDEBD5D6EB30 /* Pods_Runner.framework */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 97C146E61CF9000F007C117D /* Project object */; - proxyType = 1; - remoteGlobalIDString = 97C146ED1CF9000F007C117D; - remoteInfo = Runner; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 9705A1C41CF9048500538489 /* Embed Frameworks */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Embed Frameworks"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; - 22428D486F110EE0B969469D /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; - 253C5EA6840355311DB030EA /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 2AFAE479A87ECDEBD5D6EB30 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 2C0D722D2ED971BF672D18D5 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; - 544621C7727C798253BAB2C8 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 7ABF0EC746A2D686A0ED574F /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; - 877FDC97D8B87080E35B3EB7 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; - 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; - 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; - 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; - 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; - 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; - B14AB50E8716720E10D074BD /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; - D3AD250AADBF93406007C9EB /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 759A57780A409ED209817654 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - E44A9405B1EB1B638DD05A58 /* Pods_RunnerTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EB1CF9000F007C117D /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - FF49F60EC38658783D8D66DA /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 1454C118FFCECEEDF59152D2 /* Pods */ = { - isa = PBXGroup; - children = ( - 253C5EA6840355311DB030EA /* Pods-Runner.debug.xcconfig */, - 22428D486F110EE0B969469D /* Pods-Runner.release.xcconfig */, - D3AD250AADBF93406007C9EB /* Pods-Runner.profile.xcconfig */, - 2C0D722D2ED971BF672D18D5 /* Pods-RunnerTests.debug.xcconfig */, - 877FDC97D8B87080E35B3EB7 /* Pods-RunnerTests.release.xcconfig */, - 544621C7727C798253BAB2C8 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - 20A3C64D2B1CFED5A81C3251 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 2AFAE479A87ECDEBD5D6EB30 /* Pods_Runner.framework */, - 7ABF0EC746A2D686A0ED574F /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; - 331C8082294A63A400263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C807B294A618700263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 9740EEB11CF90186004384FC /* Flutter */ = { - isa = PBXGroup; - children = ( - 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 9740EEB31CF90195004384FC /* Generated.xcconfig */, - ); - name = Flutter; - sourceTree = ""; - }; - 97C146E51CF9000F007C117D = { - isa = PBXGroup; - children = ( - 9740EEB11CF90186004384FC /* Flutter */, - 97C146F01CF9000F007C117D /* Runner */, - 97C146EF1CF9000F007C117D /* Products */, - 331C8082294A63A400263BE5 /* RunnerTests */, - 1454C118FFCECEEDF59152D2 /* Pods */, - 20A3C64D2B1CFED5A81C3251 /* Frameworks */, - B14AB50E8716720E10D074BD /* GoogleService-Info.plist */, - ); - sourceTree = ""; - }; - 97C146EF1CF9000F007C117D /* Products */ = { - isa = PBXGroup; - children = ( - 97C146EE1CF9000F007C117D /* Runner.app */, - 331C8081294A63A400263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 97C146F01CF9000F007C117D /* Runner */ = { - isa = PBXGroup; - children = ( - 97C146FA1CF9000F007C117D /* Main.storyboard */, - 97C146FD1CF9000F007C117D /* Assets.xcassets */, - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, - 97C147021CF9000F007C117D /* Info.plist */, - 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, - 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, - 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, - 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, - ); - path = Runner; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C8080294A63A400263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - B9A66CAAF434B6A1BD8C4E09 /* [CP] Check Pods Manifest.lock */, - 331C807D294A63A400263BE5 /* Sources */, - 331C807F294A63A400263BE5 /* Resources */, - 759A57780A409ED209817654 /* Frameworks */, - ); - 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 = ( - C1C48B0232C0B26BFF405512 /* [CP] Check Pods Manifest.lock */, - 9740EEB61CF901F6004384FC /* Run Script */, - 97C146EA1CF9000F007C117D /* Sources */, - 97C146EB1CF9000F007C117D /* Frameworks */, - 97C146EC1CF9000F007C117D /* Resources */, - 9705A1C41CF9048500538489 /* Embed Frameworks */, - 3B06AD1E1E4923F5004D2608 /* Thin Binary */, - 33590C9CD073D3D5EBA02CDE /* [CP] Embed Pods Frameworks */, - 7A77858F6F15CB76D2D3A872 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */, - ); - 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 = { - BuildIndependentTargetsInParallel = YES; - LastUpgradeCheck = 1510; - 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 */, - F2A3345EC3021060731668D3 /* GoogleService-Info.plist in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 33590C9CD073D3D5EBA02CDE /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - 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"; - }; - 7A77858F6F15CB76D2D3A872 /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\""; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:$HOME/.pub-cache/bin\"\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PODS_ROOT/FirebaseCrashlytics/upload-symbols\" --platform=ios --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n"; - }; - 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"; - }; - B9A66CAAF434B6A1BD8C4E09 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - C1C48B0232C0B26BFF405512 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C807D294A63A400263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 97C146EA1CF9000F007C117D /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, - 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 97C146ED1CF9000F007C117D /* Runner */; - targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 97C146FA1CF9000F007C117D /* Main.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C146FB1CF9000F007C117D /* Base */, - ); - name = Main.storyboard; - sourceTree = ""; - }; - 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { - isa = PBXVariantGroup; - children = ( - 97C147001CF9000F007C117D /* Base */, - ); - name = LaunchScreen.storyboard; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 249021D3217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - 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; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Profile; - }; - 249021D4217E4FDB00AE95B9 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - 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.syncrowWeb; - 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 = 2C0D722D2ED971BF672D18D5 /* 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.syncrowWeb.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 = 877FDC97D8B87080E35B3EB7 /* 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.syncrowWeb.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 = 544621C7727C798253BAB2C8 /* 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.syncrowWeb.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; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - 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; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = iphoneos; - TARGETED_DEVICE_FAMILY = "1,2"; - }; - name = Debug; - }; - 97C147041CF9000F007C117D /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - 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; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu99; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNDECLARED_SELECTOR = YES; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 12.0; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = iphoneos; - SUPPORTED_PLATFORMS = iphoneos; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - TARGETED_DEVICE_FAMILY = "1,2"; - VALIDATE_PRODUCT = YES; - }; - name = Release; - }; - 97C147061CF9000F007C117D /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - 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.syncrowWeb; - 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.syncrowWeb; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; - SWIFT_VERSION = 5.0; - VERSIONING_SYSTEM = "apple-generic"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C8088294A63A400263BE5 /* Debug */, - 331C8089294A63A400263BE5 /* Release */, - 331C808A294A63A400263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147031CF9000F007C117D /* Debug */, - 97C147041CF9000F007C117D /* Release */, - 249021D3217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 97C147061CF9000F007C117D /* Debug */, - 97C147071CF9000F007C117D /* Release */, - 249021D4217E4FDB00AE95B9 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 97C146E61CF9000F007C117D /* Project object */; -} diff --git a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 919434a6..00000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,7 +0,0 @@ - - - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index 8e3ca5df..00000000 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner.xcworkspace/contents.xcworkspacedata b/ios/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14..00000000 --- a/ios/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings deleted file mode 100644 index f9b0d7c5..00000000 --- a/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings +++ /dev/null @@ -1,8 +0,0 @@ - - - - - PreviewsEnabled - - - diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift deleted file mode 100644 index b6363034..00000000 --- a/ios/Runner/AppDelegate.swift +++ /dev/null @@ -1,13 +0,0 @@ -import UIKit -import Flutter - -@main -@objc class AppDelegate: FlutterAppDelegate { - override func application( - _ application: UIApplication, - didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? - ) -> Bool { - GeneratedPluginRegistrant.register(with: self) - return super.application(application, didFinishLaunchingWithOptions: launchOptions) - } -} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index d36b1fab..00000000 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,122 +0,0 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png deleted file mode 100644 index dc9ada47..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png deleted file mode 100644 index 7353c41e..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png deleted file mode 100644 index 797d452e..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png deleted file mode 100644 index 6ed2d933..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png deleted file mode 100644 index 4cd7b009..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png deleted file mode 100644 index fe730945..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png deleted file mode 100644 index 321773cd..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png deleted file mode 100644 index 797d452e..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png deleted file mode 100644 index 502f463a..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png deleted file mode 100644 index 0ec30343..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png deleted file mode 100644 index 0ec30343..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png deleted file mode 100644 index e9f5fea2..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png deleted file mode 100644 index 84ac32ae..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png deleted file mode 100644 index 8953cba0..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png deleted file mode 100644 index 0467bf12..00000000 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json deleted file mode 100644 index 0bedcf2f..00000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "idiom" : "universal", - "filename" : "LaunchImage.png", - "scale" : "1x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@2x.png", - "scale" : "2x" - }, - { - "idiom" : "universal", - "filename" : "LaunchImage@3x.png", - "scale" : "3x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png deleted file mode 100644 index 9da19eac..00000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png deleted file mode 100644 index 9da19eac..00000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png deleted file mode 100644 index 9da19eac..00000000 Binary files a/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and /dev/null differ diff --git a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md deleted file mode 100644 index 89c2725b..00000000 --- a/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md +++ /dev/null @@ -1,5 +0,0 @@ -# Launch Screen Assets - -You can customize the launch screen with your own desired assets by replacing the image files in this directory. - -You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. \ No newline at end of file diff --git a/ios/Runner/Base.lproj/LaunchScreen.storyboard b/ios/Runner/Base.lproj/LaunchScreen.storyboard deleted file mode 100644 index f2e259c7..00000000 --- a/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ /dev/null @@ -1,37 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/Base.lproj/Main.storyboard b/ios/Runner/Base.lproj/Main.storyboard deleted file mode 100644 index f3c28516..00000000 --- a/ios/Runner/Base.lproj/Main.storyboard +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/ios/Runner/GoogleService-Info.plist b/ios/Runner/GoogleService-Info.plist deleted file mode 100644 index 9cdebed0..00000000 --- a/ios/Runner/GoogleService-Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - API_KEY - AIzaSyABnpH6yo2RRjtkp4PlvtK84hKwRm2DhBw - GCM_SENDER_ID - 427332280600 - PLIST_VERSION - 1 - BUNDLE_ID - com.example.syncrowWeb - PROJECT_ID - test2-8a3d2 - STORAGE_BUCKET - test2-8a3d2.firebasestorage.app - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - 1:427332280600:ios:14346b200780dc760c7e6d - DATABASE_URL - https://test2-8a3d2-default-rtdb.firebaseio.com - - \ No newline at end of file diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist deleted file mode 100644 index ba673060..00000000 --- a/ios/Runner/Info.plist +++ /dev/null @@ -1,49 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleDisplayName - Syncrow Web - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - syncrow_web - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleSignature - ???? - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSRequiresIPhoneOS - - UILaunchStoryboardName - LaunchScreen - UIMainStoryboardFile - Main - UISupportedInterfaceOrientations - - UIInterfaceOrientationPortrait - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - UISupportedInterfaceOrientations~ipad - - UIInterfaceOrientationPortrait - UIInterfaceOrientationPortraitUpsideDown - UIInterfaceOrientationLandscapeLeft - UIInterfaceOrientationLandscapeRight - - CADisableMinimumFrameDurationOnPhone - - UIApplicationSupportsIndirectInputEvents - - - diff --git a/ios/Runner/Runner-Bridging-Header.h b/ios/Runner/Runner-Bridging-Header.h deleted file mode 100644 index 308a2a56..00000000 --- a/ios/Runner/Runner-Bridging-Header.h +++ /dev/null @@ -1 +0,0 @@ -#import "GeneratedPluginRegistrant.h" diff --git a/ios/RunnerTests/RunnerTests.swift b/ios/RunnerTests/RunnerTests.swift deleted file mode 100644 index 86a7c3b1..00000000 --- a/ios/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import Flutter -import UIKit -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/lib/common/tag_dialog_textfield_dropdown.dart b/lib/common/tag_dialog_textfield_dropdown.dart index 219e03ce..9fa85284 100644 --- a/lib/common/tag_dialog_textfield_dropdown.dart +++ b/lib/common/tag_dialog_textfield_dropdown.dart @@ -17,7 +17,8 @@ class TagDialogTextfieldDropdown extends StatefulWidget { }) : super(key: key); @override - _DialogTextfieldDropdownState createState() => _DialogTextfieldDropdownState(); + _DialogTextfieldDropdownState createState() => + _DialogTextfieldDropdownState(); } class _DialogTextfieldDropdownState extends State { @@ -36,6 +37,12 @@ class _DialogTextfieldDropdownState extends State { _focusNode.addListener(() { if (!_focusNode.hasFocus) { + // Call onSelected when focus is lost + final selectedTag = _filteredItems.firstWhere( + (tag) => tag.tag == _controller.text, + orElse: () => Tag(tag: _controller.text), + ); + widget.onSelected(selectedTag); _closeDropdown(); } }); @@ -43,7 +50,9 @@ class _DialogTextfieldDropdownState extends State { void _filterItems() { setState(() { - _filteredItems = widget.items.where((tag) => tag.product?.uuid == widget.product).toList(); + _filteredItems = widget.items; + // .where((tag) => tag.product?.uuid == widget.product) + // .toList(); }); } @@ -112,7 +121,9 @@ class _DialogTextfieldDropdownState extends State { style: Theme.of(context) .textTheme .bodyMedium - ?.copyWith(color: ColorsManager.textPrimaryColor)), + ?.copyWith( + color: ColorsManager + .textPrimaryColor)), onTap: () { _controller.text = tag.tag ?? ''; widget.onSelected(tag); @@ -156,13 +167,15 @@ class _DialogTextfieldDropdownState extends State { controller: _controller, focusNode: _focusNode, onFieldSubmitted: (value) { - final selectedTag = _filteredItems.firstWhere((tag) => tag.tag == value, + final selectedTag = _filteredItems.firstWhere( + (tag) => tag.tag == value, orElse: () => Tag(tag: value)); widget.onSelected(selectedTag); _closeDropdown(); }, onTapOutside: (event) { - widget.onSelected(_filteredItems.firstWhere((tag) => tag.tag == _controller.text, + widget.onSelected(_filteredItems.firstWhere( + (tag) => tag.tag == _controller.text, orElse: () => Tag(tag: _controller.text))); _closeDropdown(); }, diff --git a/lib/pages/access_management/bloc/access_bloc.dart b/lib/pages/access_management/bloc/access_bloc.dart index 562bd5b5..dd82d739 100644 --- a/lib/pages/access_management/bloc/access_bloc.dart +++ b/lib/pages/access_management/bloc/access_bloc.dart @@ -267,7 +267,8 @@ class AccessBloc extends Bloc { selectedIndex = 0; effectiveTimeTimeStamp = null; expirationTimeTimeStamp = null; - add(FetchTableData()); + filteredData = List.from(data); + emit(TableLoaded(filteredData)); } String timestampToDate(dynamic timestamp) { diff --git a/lib/pages/analytics/helpers/dashed_border_painter.dart b/lib/pages/analytics/helpers/dashed_border_painter.dart new file mode 100644 index 00000000..410cadfd --- /dev/null +++ b/lib/pages/analytics/helpers/dashed_border_painter.dart @@ -0,0 +1,56 @@ +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +class DashedBorderPainter extends CustomPainter { + final double dashWidth; + final double dashSpace; + final Color color; + + DashedBorderPainter({ + this.dashWidth = 4.0, + this.dashSpace = 2.0, + this.color = Colors.black, + }); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = 0.5 + ..style = PaintingStyle.stroke; + + final Path topPath = Path() + ..moveTo(0, 0) + ..lineTo(size.width, 0); + + final Path bottomPath = Path() + ..moveTo(0, size.height) + ..lineTo(size.width, size.height); + + final dashedTopPath = _createDashedPath(topPath, dashWidth, dashSpace); + final dashedBottomPath = _createDashedPath(bottomPath, dashWidth, dashSpace); + + canvas.drawPath(dashedTopPath, paint); + canvas.drawPath(dashedBottomPath, paint); + } + + Path _createDashedPath(Path source, double dashWidth, double dashSpace) { + final Path dashedPath = Path(); + for (PathMetric pathMetric in source.computeMetrics()) { + double distance = 0.0; + while (distance < pathMetric.length) { + final double nextDistance = distance + dashWidth; + dashedPath.addPath( + pathMetric.extractPath(distance, nextDistance), + Offset.zero, + ); + distance = nextDistance + dashSpace; + } + } + return dashedPath; + } + + @override + bool shouldRepaint(covariant CustomPainter oldDelegate) => false; +} diff --git a/lib/pages/analytics/models/air_quality_data_model.dart b/lib/pages/analytics/models/air_quality_data_model.dart new file mode 100644 index 00000000..38b72abc --- /dev/null +++ b/lib/pages/analytics/models/air_quality_data_model.dart @@ -0,0 +1,56 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class AirQualityDataModel extends Equatable { + const AirQualityDataModel({ + required this.date, + required this.data, + }); + + final DateTime date; + final List data; + + factory AirQualityDataModel.fromJson(Map json) { + return AirQualityDataModel( + date: DateTime.parse(json['date'] as String), + data: (json['data'] as List) + .map( + (e) => AirQualityPercentageData.fromJson(e as Map), + ) + .toList(), + ); + } + + static final Map metricColors = { + 'good': ColorsManager.goodGreen.withValues(alpha: 0.7), + 'moderate': ColorsManager.moderateYellow.withValues(alpha: 0.7), + 'unhealthy_sensitive': ColorsManager.poorOrange.withValues(alpha: 0.7), + 'unhealthy': ColorsManager.unhealthyRed.withValues(alpha: 0.7), + 'very_unhealthy': ColorsManager.severePink.withValues(alpha: 0.7), + 'hazardous': ColorsManager.hazardousPurple.withValues(alpha: 0.7), + }; + + @override + List get props => [date, data]; +} + +class AirQualityPercentageData extends Equatable { + const AirQualityPercentageData({ + required this.type, + required this.percentage, + }); + + final String type; + final double percentage; + + factory AirQualityPercentageData.fromJson(Map json) { + return AirQualityPercentageData( + type: json['type'] as String? ?? '', + percentage: (json['percentage'] as num?)?.toDouble() ?? 0, + ); + } + + @override + List get props => [type, percentage]; +} diff --git a/lib/pages/analytics/models/analytics_device.dart b/lib/pages/analytics/models/analytics_device.dart new file mode 100644 index 00000000..3340a41d --- /dev/null +++ b/lib/pages/analytics/models/analytics_device.dart @@ -0,0 +1,82 @@ +class AnalyticsDevice { + const AnalyticsDevice({ + required this.uuid, + required this.name, + this.createdAt, + this.updatedAt, + this.deviceTuyaUuid, + this.isActive, + this.productDevice, + this.spaceUuid, + this.latitude, + this.longitude, + }); + + final String uuid; + final String name; + final DateTime? createdAt; + final DateTime? updatedAt; + final String? deviceTuyaUuid; + final bool? isActive; + final ProductDevice? productDevice; + final String? spaceUuid; + final double? latitude; + final double? longitude; + + factory AnalyticsDevice.fromJson(Map json) { + return AnalyticsDevice( + uuid: json['uuid'] as String, + name: json['name'] as String, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt'] as String) + : null, + updatedAt: json['updatedAt'] != null + ? DateTime.parse(json['updatedAt'] as String) + : null, + deviceTuyaUuid: json['deviceTuyaUuid'] as String?, + isActive: json['isActive'] as bool?, + productDevice: json['productDevice'] != null + ? ProductDevice.fromJson(json['productDevice'] as Map) + : null, + spaceUuid: json['spaceUuid'] as String?, + latitude: json['lat'] != null ? double.parse(json['lat'] as String) : null, + longitude: json['lon'] != null ? double.parse(json['lon'] as String) : null, + ); + } +} + +class ProductDevice { + const ProductDevice({ + this.uuid, + this.createdAt, + this.updatedAt, + this.catName, + this.prodId, + this.name, + this.prodType, + }); + + final String? uuid; + final DateTime? createdAt; + final DateTime? updatedAt; + final String? catName; + final String? prodId; + final String? name; + final String? prodType; + + factory ProductDevice.fromJson(Map json) { + return ProductDevice( + uuid: json['uuid'] as String?, + createdAt: json['createdAt'] != null + ? DateTime.parse(json['createdAt'] as String) + : null, + updatedAt: json['updatedAt'] != null + ? DateTime.parse(json['updatedAt'] as String) + : null, + catName: json['catName'] as String?, + prodId: json['prodId'] as String?, + name: json['name'] as String?, + prodType: json['prodType'] as String?, + ); + } +} diff --git a/lib/pages/analytics/models/device_location_info.dart b/lib/pages/analytics/models/device_location_info.dart new file mode 100644 index 00000000..aef7eebb --- /dev/null +++ b/lib/pages/analytics/models/device_location_info.dart @@ -0,0 +1,55 @@ +import 'package:equatable/equatable.dart'; + +class DeviceLocationInfo extends Equatable { + const DeviceLocationInfo({ + this.airQuality, + this.humidity, + this.city, + this.country, + this.address, + this.temperature, + }); + + final double? airQuality; + final double? humidity; + final String? city; + final String? country; + final String? address; + final double? temperature; + + factory DeviceLocationInfo.fromJson(Map json) { + return DeviceLocationInfo( + airQuality: json['aqi'] as double?, + humidity: json['humidity'] as double?, + temperature: json['temperature'] as double?, + ); + } + + DeviceLocationInfo copyWith({ + double? airQuality, + double? humidity, + String? city, + String? country, + String? address, + double? temperature, + }) { + return DeviceLocationInfo( + airQuality: airQuality ?? this.airQuality, + humidity: humidity ?? this.humidity, + city: city ?? this.city, + country: country ?? this.country, + address: address ?? this.address, + temperature: temperature ?? this.temperature, + ); + } + + @override + List get props => [ + airQuality, + humidity, + city, + country, + address, + temperature, + ]; +} diff --git a/lib/pages/analytics/models/occupacy.dart b/lib/pages/analytics/models/occupacy.dart new file mode 100644 index 00000000..b4b8dac9 --- /dev/null +++ b/lib/pages/analytics/models/occupacy.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; + +class Occupacy extends Equatable { + final DateTime date; + final String occupancy; + final String spaceUuid; + final int occupiedSeconds; + + const Occupacy({ + required this.date, + required this.occupancy, + required this.spaceUuid, + required this.occupiedSeconds, + }); + + factory Occupacy.fromJson(Map json) { + return Occupacy( + date: DateTime.parse(json['event_date'] as String? ?? '${DateTime.now()}'), + occupancy: (json['occupancy_percentage'] ?? 0).toString(), + spaceUuid: json['space_uuid'] as String? ?? '', + occupiedSeconds: json['occupied_seconds'] as int? ?? 0, + ); + } + + @override + List get props => [ + date, + occupancy, + spaceUuid, + occupiedSeconds, + ]; +} diff --git a/lib/pages/analytics/models/occupancy_heat_map_model.dart b/lib/pages/analytics/models/occupancy_heat_map_model.dart new file mode 100644 index 00000000..73e7d5d7 --- /dev/null +++ b/lib/pages/analytics/models/occupancy_heat_map_model.dart @@ -0,0 +1,28 @@ +import 'package:equatable/equatable.dart'; + +class OccupancyHeatMapModel extends Equatable { + final String uuid; + + final DateTime eventDate; + + final int countTotalPresenceDetected; + + const OccupancyHeatMapModel({ + required this.uuid, + required this.eventDate, + required this.countTotalPresenceDetected, + }); + + factory OccupancyHeatMapModel.fromJson(Map json) { + return OccupancyHeatMapModel( + uuid: json['uuid'] as String? ?? '', + eventDate: DateTime.parse( + json['event_date'] as String? ?? '${DateTime.now()}', + ), + countTotalPresenceDetected: json['count_total_presence_detected'] as int? ?? 0, + ); + } + + @override + List get props => [uuid, eventDate, countTotalPresenceDetected]; +} diff --git a/lib/pages/analytics/models/phases_energy_consumption.dart b/lib/pages/analytics/models/phases_energy_consumption.dart index f986c3ad..a826bac0 100644 --- a/lib/pages/analytics/models/phases_energy_consumption.dart +++ b/lib/pages/analytics/models/phases_energy_consumption.dart @@ -1,27 +1,66 @@ import 'package:equatable/equatable.dart'; class PhasesEnergyConsumption extends Equatable { - final int month; - final double phaseA; - final double phaseB; - final double phaseC; + final String uuid; + final DateTime createdAt; + final DateTime updatedAt; + final String deviceUuid; + final DateTime date; + final double energyConsumedKw; + final double energyConsumedA; + final double energyConsumedB; + final double energyConsumedC; const PhasesEnergyConsumption({ - required this.month, - required this.phaseA, - required this.phaseB, - required this.phaseC, + required this.uuid, + required this.createdAt, + required this.updatedAt, + required this.deviceUuid, + required this.date, + required this.energyConsumedKw, + required this.energyConsumedA, + required this.energyConsumedB, + required this.energyConsumedC, }); @override - List get props => [month, phaseA, phaseB, phaseC]; + List get props => [ + uuid, + createdAt, + updatedAt, + deviceUuid, + date, + energyConsumedKw, + energyConsumedA, + energyConsumedB, + energyConsumedC, + ]; factory PhasesEnergyConsumption.fromJson(Map json) { return PhasesEnergyConsumption( - month: json['month'] as int, - phaseA: (json['phaseA'] as num).toDouble(), - phaseB: (json['phaseB'] as num).toDouble(), - phaseC: (json['phaseC'] as num).toDouble(), + uuid: json['uuid'] as String, + createdAt: DateTime.parse(json['createdAt'] as String), + updatedAt: DateTime.parse(json['updatedAt'] as String), + deviceUuid: json['deviceUuid'] as String, + date: DateTime.parse(json['date'] as String), + energyConsumedKw: double.parse(json['energyConsumedKw']), + energyConsumedA: double.parse(json['energyConsumedA']), + energyConsumedB: double.parse(json['energyConsumedB']), + energyConsumedC: double.parse(json['energyConsumedC']), ); } + + Map toJson() { + return { + 'uuid': uuid, + 'createdAt': createdAt.toIso8601String(), + 'updatedAt': updatedAt.toIso8601String(), + 'deviceUuid': deviceUuid, + 'date': date.toIso8601String().split('T')[0], + 'energyConsumedKw': energyConsumedKw.toString(), + 'energyConsumedA': energyConsumedA.toString(), + 'energyConsumedB': energyConsumedB.toString(), + 'energyConsumedC': energyConsumedC.toString(), + }; + } } diff --git a/lib/pages/analytics/models/range_of_aqi.dart b/lib/pages/analytics/models/range_of_aqi.dart new file mode 100644 index 00000000..0308d564 --- /dev/null +++ b/lib/pages/analytics/models/range_of_aqi.dart @@ -0,0 +1,49 @@ +import 'package:equatable/equatable.dart'; + +class RangeOfAqi extends Equatable { + final DateTime date; + final List data; + + const RangeOfAqi({ + required this.data, + required this.date, + }); + + factory RangeOfAqi.fromJson(Map json) { + return RangeOfAqi( + date: DateTime.parse(json['date'] as String), + data: (json['data'] as List) + .map((e) => RangeOfAqiValue.fromJson(e as Map)) + .toList(), + ); + } + + @override + List get props => [data, date]; +} + +class RangeOfAqiValue extends Equatable { + final String type; + final double min; + final double average; + final double max; + + const RangeOfAqiValue({ + required this.type, + required this.min, + required this.average, + required this.max, + }); + + factory RangeOfAqiValue.fromJson(Map json) { + return RangeOfAqiValue( + type: json['type'] as String, + min: (json['min'] as num).toDouble(), + average: (json['average'] as num).toDouble(), + max: (json['max'] as num).toDouble(), + ); + } + + @override + List get props => [type, min, average, max]; +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart new file mode 100644 index 00000000..40d51d2b --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart @@ -0,0 +1,62 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; + +part 'air_quality_distribution_event.dart'; +part 'air_quality_distribution_state.dart'; + +class AirQualityDistributionBloc + extends Bloc { + final AirQualityDistributionService _aqiDistributionService; + + AirQualityDistributionBloc( + this._aqiDistributionService, + ) : super(const AirQualityDistributionState()) { + on(_onLoadAirQualityDistribution); + on(_onClearAirQualityDistribution); + on(_onUpdateAqiTypeEvent); + } + + Future _onLoadAirQualityDistribution( + LoadAirQualityDistribution event, + Emitter emit, + ) async { + try { + emit(state.copyWith(status: AirQualityDistributionStatus.loading)); + final result = await _aqiDistributionService.getAirQualityDistribution( + event.param, + ); + emit( + state.copyWith( + status: AirQualityDistributionStatus.success, + chartData: result, + ), + ); + } catch (e) { + emit( + AirQualityDistributionState( + status: AirQualityDistributionStatus.failure, + errorMessage: e.toString(), + selectedAqiType: state.selectedAqiType, + ), + ); + } + } + + Future _onClearAirQualityDistribution( + ClearAirQualityDistribution event, + Emitter emit, + ) async { + emit(const AirQualityDistributionState()); + } + + void _onUpdateAqiTypeEvent( + UpdateAqiTypeEvent event, + Emitter emit, + ) { + emit(state.copyWith(selectedAqiType: event.aqiType)); + } +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart new file mode 100644 index 00000000..b91dafe5 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_event.dart @@ -0,0 +1,30 @@ +part of 'air_quality_distribution_bloc.dart'; + +sealed class AirQualityDistributionEvent extends Equatable { + const AirQualityDistributionEvent(); + + @override + List get props => []; +} + +final class LoadAirQualityDistribution extends AirQualityDistributionEvent { + final GetAirQualityDistributionParam param; + + const LoadAirQualityDistribution(this.param); + + @override + List get props => [param]; +} + +final class UpdateAqiTypeEvent extends AirQualityDistributionEvent { + const UpdateAqiTypeEvent(this.aqiType); + + final AqiType aqiType; + + @override + List get props => [aqiType]; +} + +final class ClearAirQualityDistribution extends AirQualityDistributionEvent { + const ClearAirQualityDistribution(); +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart new file mode 100644 index 00000000..0b02fd7e --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_state.dart @@ -0,0 +1,39 @@ +part of 'air_quality_distribution_bloc.dart'; + +enum AirQualityDistributionStatus { + initial, + loading, + success, + failure, +} + +class AirQualityDistributionState extends Equatable { + const AirQualityDistributionState({ + this.status = AirQualityDistributionStatus.initial, + this.chartData = const [], + this.errorMessage, + this.selectedAqiType = AqiType.aqi, + }); + + final AirQualityDistributionStatus status; + final List chartData; + final String? errorMessage; + final AqiType selectedAqiType; + + AirQualityDistributionState copyWith({ + AirQualityDistributionStatus? status, + List? chartData, + String? errorMessage, + AqiType? selectedAqiType, + }) { + return AirQualityDistributionState( + status: status ?? this.status, + chartData: chartData ?? this.chartData, + errorMessage: errorMessage ?? this.errorMessage, + selectedAqiType: selectedAqiType ?? this.selectedAqiType, + ); + } + + @override + List get props => [status, chartData, errorMessage, selectedAqiType]; +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart new file mode 100644 index 00000000..4f41eb0c --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart @@ -0,0 +1,50 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/analytics/models/device_location_info.dart'; +import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; +import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart'; + +part 'device_location_event.dart'; +part 'device_location_state.dart'; + +class DeviceLocationBloc extends Bloc { + DeviceLocationBloc( + this._deviceLocationService, + ) : super(const DeviceLocationState()) { + on(_onLoadDeviceLocation); + on(_onClearDeviceLocation); + } + + final DeviceLocationService _deviceLocationService; + + Future _onLoadDeviceLocation( + LoadDeviceLocationEvent event, + Emitter emit, + ) async { + emit(const DeviceLocationState(status: DeviceLocationStatus.loading)); + + try { + final locationInfo = await _deviceLocationService.get(event.param); + emit( + DeviceLocationState( + status: DeviceLocationStatus.success, + locationInfo: locationInfo, + ), + ); + } catch (e) { + emit( + DeviceLocationState( + status: DeviceLocationStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + void _onClearDeviceLocation( + ClearDeviceLocationEvent event, + Emitter emit, + ) { + emit(const DeviceLocationState()); + } +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_event.dart b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_event.dart new file mode 100644 index 00000000..37137e4a --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_event.dart @@ -0,0 +1,21 @@ +part of 'device_location_bloc.dart'; + +sealed class DeviceLocationEvent extends Equatable { + const DeviceLocationEvent(); + + @override + List get props => []; +} + +final class LoadDeviceLocationEvent extends DeviceLocationEvent { + const LoadDeviceLocationEvent(this.param); + + final GetDeviceLocationDataParam param; + + @override + List get props => [param]; +} + +final class ClearDeviceLocationEvent extends DeviceLocationEvent { + const ClearDeviceLocationEvent(); +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_state.dart b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_state.dart new file mode 100644 index 00000000..8f66ad28 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/device_location/device_location_state.dart @@ -0,0 +1,18 @@ +part of 'device_location_bloc.dart'; + +enum DeviceLocationStatus { initial, loading, success, failure } + +final class DeviceLocationState extends Equatable { + const DeviceLocationState({ + this.status = DeviceLocationStatus.initial, + this.locationInfo, + this.errorMessage, + }); + + final DeviceLocationStatus status; + final DeviceLocationInfo? locationInfo; + final String? errorMessage; + + @override + List get props => [status, locationInfo, errorMessage]; +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart new file mode 100644 index 00000000..88c3715e --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart @@ -0,0 +1,80 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; +import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; +import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart'; + +part 'range_of_aqi_event.dart'; +part 'range_of_aqi_state.dart'; + +class RangeOfAqiBloc extends Bloc { + RangeOfAqiBloc(this._rangeOfAqiService) : super(const RangeOfAqiState()) { + on(_onLoadRangeOfAqiEvent); + on(_onClearRangeOfAqiEvent); + on(_onUpdateAqiTypeEvent); + } + + final RangeOfAqiService _rangeOfAqiService; + + Future _onLoadRangeOfAqiEvent( + LoadRangeOfAqiEvent event, + Emitter emit, + ) async { + emit( + state.copyWith(status: RangeOfAqiStatus.loading), + ); + try { + final rangeOfAqi = await _rangeOfAqiService.load(event.param); + emit( + state.copyWith( + status: RangeOfAqiStatus.loaded, + rangeOfAqi: rangeOfAqi, + filteredRangeOfAqi: _arrangeChartDataByType( + rangeOfAqi, + state.selectedAqiType, + ), + ), + ); + } catch (e) { + emit( + state.copyWith( + status: RangeOfAqiStatus.failure, + errorMessage: '$e', + ), + ); + } + } + + void _onUpdateAqiTypeEvent( + UpdateAqiTypeEvent event, + Emitter emit, + ) { + emit( + state.copyWith( + selectedAqiType: event.aqiType, + filteredRangeOfAqi: _arrangeChartDataByType(state.rangeOfAqi, event.aqiType), + ), + ); + } + + List _arrangeChartDataByType( + List rangeOfAqi, + AqiType aqiType, + ) { + final filteredRangeOfAqi = rangeOfAqi.map( + (data) => RangeOfAqi( + date: data.date, + data: data.data.where((value) => value.type == aqiType.code).toList(), + ), + ); + return filteredRangeOfAqi.toList(); + } + + void _onClearRangeOfAqiEvent( + ClearRangeOfAqiEvent event, + Emitter emit, + ) { + emit(const RangeOfAqiState()); + } +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart new file mode 100644 index 00000000..6a08df5b --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_event.dart @@ -0,0 +1,30 @@ +part of 'range_of_aqi_bloc.dart'; + +sealed class RangeOfAqiEvent extends Equatable { + const RangeOfAqiEvent(); + + @override + List get props => []; +} + +class LoadRangeOfAqiEvent extends RangeOfAqiEvent { + const LoadRangeOfAqiEvent(this.param); + + final GetRangeOfAqiParam param; + + @override + List get props => [param]; +} + +class UpdateAqiTypeEvent extends RangeOfAqiEvent { + const UpdateAqiTypeEvent(this.aqiType); + + final AqiType aqiType; + + @override + List get props => [aqiType]; +} + +class ClearRangeOfAqiEvent extends RangeOfAqiEvent { + const ClearRangeOfAqiEvent(); +} diff --git a/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart new file mode 100644 index 00000000..9308020c --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_state.dart @@ -0,0 +1,39 @@ +part of 'range_of_aqi_bloc.dart'; + +enum RangeOfAqiStatus { initial, loading, loaded, failure } + +final class RangeOfAqiState extends Equatable { + const RangeOfAqiState({ + this.rangeOfAqi = const [], + this.filteredRangeOfAqi = const [], + this.status = RangeOfAqiStatus.initial, + this.errorMessage, + this.selectedAqiType = AqiType.aqi, + }); + + final RangeOfAqiStatus status; + final List rangeOfAqi; + final List filteredRangeOfAqi; + final String? errorMessage; + final AqiType selectedAqiType; + + RangeOfAqiState copyWith({ + RangeOfAqiStatus? status, + List? rangeOfAqi, + List? filteredRangeOfAqi, + String? errorMessage, + AqiType? selectedAqiType, + }) { + return RangeOfAqiState( + status: status ?? this.status, + rangeOfAqi: rangeOfAqi ?? this.rangeOfAqi, + filteredRangeOfAqi: filteredRangeOfAqi ?? this.filteredRangeOfAqi, + errorMessage: errorMessage ?? this.errorMessage, + selectedAqiType: selectedAqiType ?? this.selectedAqiType, + ); + } + + @override + List get props => + [status, rangeOfAqi, filteredRangeOfAqi, errorMessage, selectedAqiType]; +} diff --git a/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart new file mode 100644 index 00000000..223c0357 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; +import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; +import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; +import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; + +abstract final class FetchAirQualityDataHelper { + const FetchAirQualityDataHelper._(); + + static void loadAirQualityData( + BuildContext context, { + required DateTime date, + required String communityUuid, + required String spaceUuid, + bool shouldFetchAnalyticsDevices = true, + }) { + final date = context.read().state.monthlyDate; + final aqiType = context.read().state.selectedAqiType; + loadAnalyticsDevices( + context, + communityUuid: communityUuid, + spaceUuid: spaceUuid, + ); + loadRangeOfAqi( + context, + spaceUuid: spaceUuid, + date: date, + ); + loadAirQualityDistribution( + context, + spaceUuid: spaceUuid, + date: date, + aqiType: aqiType, + ); + } + + static void clearAllData(BuildContext context) { + context.read().add( + const ClearAnalyticsDeviceEvent(), + ); + context.read().add( + const RealtimeDeviceChangesClosed(), + ); + context.read().add( + const ClearAirQualityDistribution(), + ); + context.read().add(const ClearRangeOfAqiEvent()); + + context.read().add(const ClearDeviceLocationEvent()); + } + + static void loadAnalyticsDevices( + BuildContext context, { + required String communityUuid, + required String spaceUuid, + }) { + context.read().add( + LoadAnalyticsDevicesEvent( + param: GetAnalyticsDevicesParam( + communityUuid: communityUuid, + spaceUuid: spaceUuid, + deviceTypes: ['AQI'], + requestType: AnalyticsDeviceRequestType.occupancy, + ), + onSuccess: (device) { + context.read() + ..add(const RealtimeDeviceChangesClosed()) + ..add(RealtimeDeviceChangesStarted(device.uuid)); + + context.read().add( + LoadDeviceLocationEvent( + GetDeviceLocationDataParam( + latitude: device.latitude ?? 0, + longitude: device.longitude ?? 0, + ), + ), + ); + }, + ), + ); + } + + static void loadRangeOfAqi( + BuildContext context, { + required String spaceUuid, + required DateTime date, + }) { + context.read().add( + LoadRangeOfAqiEvent( + GetRangeOfAqiParam( + date: date, + spaceUuid: spaceUuid, + ), + ), + ); + } + + static void loadAirQualityDistribution( + BuildContext context, { + required String spaceUuid, + required DateTime date, + required AqiType aqiType, + }) { + context.read().add( + LoadAirQualityDistribution( + GetAirQualityDistributionParam( + spaceUuid: spaceUuid, + date: date, + aqiType: aqiType, + ), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart new file mode 100644 index 00000000..21cb2a9e --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart @@ -0,0 +1,115 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +abstract final class RangeOfAqiChartsHelper { + const RangeOfAqiChartsHelper._(); + + static const gradientData = <(Color color, String label)>[ + (ColorsManager.goodGreen, 'Good'), + (ColorsManager.moderateYellow, 'Moderate'), + (ColorsManager.poorOrange, 'Poor'), + (ColorsManager.unhealthyRed, 'Unhealthy'), + (ColorsManager.severePink, 'Severe'), + (ColorsManager.hazardousPurple, 'Hazardous'), + ]; + + static FlTitlesData titlesData(BuildContext context, List data) { + final titlesData = EnergyManagementChartsHelper.titlesData(context); + return titlesData.copyWith( + bottomTitles: titlesData.bottomTitles.copyWith( + sideTitles: titlesData.bottomTitles.sideTitles.copyWith( + getTitlesWidget: (value, meta) => Padding( + padding: const EdgeInsetsDirectional.only(top: 20.0), + child: Text( + data.isNotEmpty ? data[value.toInt()].date.day.toString() : '', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ), + ), + ), + leftTitles: titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + interval: 50, + maxIncluded: false, + getTitlesWidget: (value, meta) { + final text = value >= 300 ? '301+' : value.toInt().toString(); + return Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: Text( + text, + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ), + ); + }, + ), + ), + ); + } + + static List getTooltipItems( + List touchedSpots, + List chartData, + ) { + return touchedSpots.asMap().entries.map((entry) { + final index = entry.key; + final spot = entry.value; + + final label = switch (spot.barIndex) { + 0 => 'Max', + 1 => 'Avg', + 2 => 'Min', + _ => '', + }; + + final date = DateFormat('dd/MM').format(chartData[spot.x.toInt()].date); + + return LineTooltipItem( + index == 0 ? '$date\n' : '', + const TextStyle( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w600, + fontSize: 12, + ), + children: [ + TextSpan(text: '$label: ${spot.y.toStringAsFixed(0)}'), + ], + ); + }).toList(); + } + + static LineTouchData lineTouchData( + List chartData, + ) { + return LineTouchData( + touchTooltipData: LineTouchTooltipData( + getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors, + tooltipBorder: const BorderSide( + color: ColorsManager.semiTransparentBlack, + ), + tooltipRoundedRadius: 16, + showOnTopOfTheChartBoxArea: false, + tooltipPadding: const EdgeInsets.all(8), + getTooltipItems: (touchedSpots) => RangeOfAqiChartsHelper.getTooltipItems( + touchedSpots, + chartData, + ), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart new file mode 100644 index 00000000..b6d403eb --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/views/air_quality_view.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart'; + +class AirQualityView extends StatelessWidget { + const AirQualityView({super.key}); + + static const _padding = EdgeInsetsDirectional.all(32); + + @override + Widget build(BuildContext context) { + return LayoutBuilder( + builder: (context, constraints) { + final isMediumOrLess = constraints.maxWidth <= 900; + final height = MediaQuery.sizeOf(context).height; + if (isMediumOrLess) { + return SingleChildScrollView( + padding: _padding, + child: Column( + spacing: 32, + children: [ + SizedBox( + height: height * 1.2, + child: const AirQualityEndSideWidget(), + ), + SizedBox( + height: height * 0.5, + child: const RangeOfAqiChartBox(), + ), + SizedBox( + height: height * 0.5, + child: const AqiDistributionChartBox(), + ), + ], + ), + ); + } + + return SingleChildScrollView( + child: Container( + padding: _padding, + height: height * 1.1, + child: const Column( + children: [ + Expanded( + child: Row( + spacing: 32, + children: [ + Expanded( + flex: 10, + child: Column( + spacing: 20, + children: [ + Expanded(child: RangeOfAqiChartBox()), + Expanded(child: AqiDistributionChartBox()), + ], + ), + ), + Expanded(flex: 6, child: AirQualityEndSideWidget()), + ], + ), + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart new file mode 100644 index 00000000..93904604 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart'; + +class AirQualityEndSideGaugeAndInfo extends StatelessWidget { + const AirQualityEndSideGaugeAndInfo({ + super.key, + required this.temperature, + required this.humidity, + required this.aqiLevel, + }); + + final int temperature; + final int humidity; + final String aqiLevel; + + @override + Widget build(BuildContext context) { + return Expanded( + flex: 2, + child: Row( + children: [ + Expanded( + flex: 2, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [Expanded(child: AqiGauge(aqi: aqi))], + ), + ), + const Spacer(), + Expanded( + child: Padding( + padding: const EdgeInsetsDirectional.only(end: 20), + child: AqiHumidityAndTemperature( + temperature: temperature, + humidity: humidity, + ), + ), + ), + ], + ), + ); + } + + double get aqi => switch (aqiLevel) { + 'level_1' => 25.0, + 'level_2' => 75.0, + 'level_3' => 125.0, + _ => 0.0, + }; +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.dart new file mode 100644 index 00000000..da8dd86a --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AirQualityEndSideLiveIndicator extends StatelessWidget { + const AirQualityEndSideLiveIndicator({super.key}); + + @override + Widget build(BuildContext context) { + return Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Entrance', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + const Spacer(), + CircleAvatar( + backgroundColor: ColorsManager.green.withValues( + alpha: 0.5, + ), + radius: 2, + ), + const SizedBox(width: 4), + Text( + 'Live', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.green.withValues(alpha: 0.5), + fontWeight: FontWeight.w400, + fontSize: 8, + ), + ), + ], + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart new file mode 100644 index 00000000..6e182e18 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/air_quality_end_side_widget.dart @@ -0,0 +1,28 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_sidebar_header.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AirQualityEndSideWidget extends StatelessWidget { + const AirQualityEndSideWidget({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(30), + ), + padding: const EdgeInsetsDirectional.all(32), + child: const Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnalyticsSidebarHeader(title: 'AQI Sensor'), + Expanded(flex: 15, child: AqiDeviceInfo()), + SizedBox(height: 20), + Expanded(flex: 6, child: AqiLocationInfo()), + ], + ), + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart new file mode 100644 index 00000000..ebe88614 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_device_info.dart @@ -0,0 +1,128 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_gauge_and_info.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/air_quality_end_side_live_indicator.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AqiDeviceInfo extends StatelessWidget { + const AqiDeviceInfo({super.key}); + + String _getValueForStatus( + List deviceStatusList, + String code, { + double defaultValue = 0, + String Function(int value)? formatter, + }) { + try { + final foundStatus = deviceStatusList.firstWhere((e) => e.code == code); + final value = foundStatus.value.toString(); + final intValue = int.parse(value); + return formatter != null ? formatter(intValue) : intValue.toString(); + } catch (e) { + return defaultValue.toString(); + } + } + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final status = state.deviceStatusList; + final humidityValue = _getValueForStatus( + status, + 'humidity_value', + formatter: (value) => value.toStringAsFixed(0), + ); + + final tempValue = _getValueForStatus( + status, + 'temp_current', + formatter: (value) => (value / 10).toStringAsFixed(0), + ); + final pm25Value = _getValueForStatus( + status, + 'pm25_value', + formatter: (value) => value.toString().padLeft(3, '0'), + ); + final pm10Value = _getValueForStatus( + status, + 'pm10', + formatter: (value) => value.toString().padLeft(3, '0'), + ); + final co2Value = _getValueForStatus( + status, + 'co2_value', + formatter: (value) => value.toString().padLeft(4, '0'), + ); + final ch2oValue = _getValueForStatus( + status, + 'ch2o_value', + formatter: (value) => (value / 100).toStringAsFixed(2), + ); + final tvocValue = _getValueForStatus( + status, + 'tvoc_value', + formatter: (value) => (value / 100).toStringAsFixed(2), + ); + + return Container( + decoration: secondarySection.copyWith(boxShadow: const []), + padding: const EdgeInsetsDirectional.all(20), + child: Column( + spacing: 6, + mainAxisSize: MainAxisSize.max, + children: [ + const AirQualityEndSideLiveIndicator(), + AirQualityEndSideGaugeAndInfo( + aqiLevel: status + .firstWhere( + (e) => e.code == 'air_quality_index', + orElse: () => Status(code: 'air_quality_index', value: ''), + ) + .value + .toString(), + temperature: int.parse(tempValue), + humidity: int.parse(humidityValue), + ), + const SizedBox(height: 20), + AqiSubValueWidget( + range: (0, 999), + label: AqiType.pm25.value, + value: pm25Value, + unit: AqiType.pm25.unit, + ), + AqiSubValueWidget( + range: (0, 999), + label: AqiType.pm10.value, + value: pm10Value, + unit: AqiType.pm10.unit, + ), + AqiSubValueWidget( + range: (0, 5), + label: AqiType.hcho.value, + value: ch2oValue, + unit: AqiType.hcho.unit, + ), + AqiSubValueWidget( + range: (0, 999), + label: AqiType.tvoc.value, + value: tvocValue, + unit: AqiType.tvoc.unit, + ), + AqiSubValueWidget( + range: (0, 5000), + label: AqiType.co2.value, + value: co2Value, + unit: AqiType.co2.unit, + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart new file mode 100644 index 00000000..2f3d7ff0 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart @@ -0,0 +1,164 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AqiDistributionChart extends StatelessWidget { + const AqiDistributionChart({super.key, required this.chartData}); + final List chartData; + + static const _rodStackItemsSpacing = 0.4; + static const _barWidth = 13.0; + static final _barBorderRadius = BorderRadius.circular(22); + + @override + Widget build(BuildContext context) { + return BarChart( + BarChartData( + maxY: 100.1, + gridData: EnergyManagementChartsHelper.gridData( + horizontalInterval: 20, + ), + borderData: EnergyManagementChartsHelper.borderData(), + barTouchData: _barTouchData(context), + titlesData: _titlesData(context), + barGroups: _buildBarGroups(), + ), + duration: Duration.zero, + ); + } + + List _buildBarGroups() { + return List.generate(chartData.length, (index) { + final data = chartData[index]; + final stackItems = []; + double currentY = 0; + var isFirstElement = true; + + for (final percentageData in data.data) { + stackItems.add( + BarChartRodData( + fromY: currentY, + toY: currentY + percentageData.percentage, + color: AirQualityDataModel.metricColors[percentageData.type], + borderRadius: isFirstElement + ? const BorderRadius.only( + topLeft: Radius.circular(22), + topRight: Radius.circular(22), + ) + : _barBorderRadius, + width: _barWidth, + ), + ); + currentY += percentageData.percentage + _rodStackItemsSpacing; + isFirstElement = false; + } + + return BarChartGroupData( + x: index, + barRods: stackItems, + groupVertically: true, + ); + }); + } + + BarTouchData _barTouchData(BuildContext context) { + return BarTouchData( + touchTooltipData: BarTouchTooltipData( + getTooltipColor: (_) => ColorsManager.whiteColors, + tooltipBorder: const BorderSide( + color: ColorsManager.semiTransparentBlack, + ), + tooltipRoundedRadius: 16, + tooltipPadding: const EdgeInsets.all(8), + getTooltipItem: (group, groupIndex, rod, rodIndex) { + final data = chartData[group.x]; + + final children = []; + + final textStyle = context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontSize: 8, + ); + + for (final percentageData in data.data) { + final percentage = percentageData.percentage.toStringAsFixed(1); + final type = percentageData.type[0].toUpperCase() + + percentageData.type.substring(1).replaceAll('_', ' '); + children.add(TextSpan( + text: '\n$type: $percentage%', + style: textStyle, + )); + } + + return BarTooltipItem( + DateFormat('dd/MM/yyyy').format(data.date), + context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.blackColor, + fontSize: 9, + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.start, + children: children, + ); + }, + ), + ); + } + + FlTitlesData _titlesData(BuildContext context) { + final titlesData = EnergyManagementChartsHelper.titlesData( + context, + leftTitlesInterval: 20, + ); + + final leftTitles = titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + interval: 20, + maxIncluded: false, + minIncluded: true, + getTitlesWidget: (value, meta) => Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: Text( + '${value.toStringAsFixed(0)}%', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.lightGreyColor, + ), + ), + ), + ), + ), + ); + + final bottomTitles = AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, _) => FittedBox( + alignment: AlignmentDirectional.bottomCenter, + fit: BoxFit.scaleDown, + child: Text( + chartData[value.toInt()].date.day.toString(), + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGreyColor, + fontSize: 8, + ), + ), + ), + reservedSize: 36, + ), + ); + + return titlesData.copyWith( + leftTitles: leftTitles, + bottomTitles: bottomTitles, + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart new file mode 100644 index 00000000..25cfd19d --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_box.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AqiDistributionChartBox extends StatelessWidget { + const AqiDistributionChartBox({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Container( + padding: const EdgeInsetsDirectional.all(30), + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(30), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (state.errorMessage != null) ...[ + AnalyticsErrorWidget(state.errorMessage), + const SizedBox(height: 10), + ], + AqiDistributionChartTitle( + isLoading: state.status == AirQualityDistributionStatus.loading, + ), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Expanded( + child: AqiDistributionChart(chartData: state.chartData), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart new file mode 100644 index 00000000..926d28e1 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_distribution_chart_title.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; + +class AqiDistributionChartTitle extends StatelessWidget { + const AqiDistributionChartTitle({required this.isLoading, super.key}); + + final bool isLoading; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + ChartsLoadingWidget(isLoading: isLoading), + const Expanded( + flex: 3, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: ChartTitle( + title: Text('Distribution over Air Quality Index'), + ), + ), + ), + FittedBox( + alignment: AlignmentDirectional.centerEnd, + fit: BoxFit.scaleDown, + child: AqiTypeDropdown( + onChanged: (value) { + if (value != null) { + final bloc = context.read(); + try { + final param = _makeLoadAqiDistributionParam(context, value); + bloc.add(LoadAirQualityDistribution(param)); + } catch (_) { + return; + } finally { + bloc.add(UpdateAqiTypeEvent(value)); + } + } + }, + ), + ), + ], + ); + } + + GetAirQualityDistributionParam _makeLoadAqiDistributionParam( + BuildContext context, + AqiType aqiType, + ) { + final date = context.read().state.monthlyDate; + final spaceUuid = + context.read().state.selectedSpaces.firstOrNull ?? ''; + if (spaceUuid.isEmpty) throw Exception('Space UUID is empty'); + return GetAirQualityDistributionParam( + date: date, + spaceUuid: spaceUuid, + aqiType: aqiType, + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart new file mode 100644 index 00000000..fc7f923a --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_gauge.dart @@ -0,0 +1,115 @@ +import 'package:flutter/material.dart'; +import 'package:gauge_indicator/gauge_indicator.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AqiGauge extends StatelessWidget { + const AqiGauge({super.key, required this.aqi}); + + final double aqi; + + static const _minRange = 0.0; + static const _goodRange = 50.0; + static const _moderateRange = 100.0; + static const _maxRange = 150.0; + + @override + Widget build(BuildContext context) { + final (status, statusColor) = _getStatusData(aqi); + return AnimatedRadialGauge( + value: aqi, + debug: false, + duration: const Duration(milliseconds: 500), + curve: Curves.easeInOut, + initialValue: 0, + alignment: Alignment.bottomCenter, + builder: (context, child, value) { + return Align( + alignment: AlignmentDirectional.bottomCenter, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomCenter, + child: Text.rich( + TextSpan( + text: 'Air Quality\n', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + children: [ + TextSpan( + text: status, + style: context.textTheme.bodySmall?.copyWith( + color: _darkenColor(statusColor), + fontWeight: FontWeight.w400, + fontSize: 30, + ), + ), + ], + ), + textAlign: TextAlign.center, + ), + ), + ); + }, + axis: GaugeAxis( + progressBar: const GaugeProgressBar.basic(color: Colors.transparent), + style: const GaugeAxisStyle( + cornerRadius: Radius.circular(16), + thickness: 14, + segmentSpacing: 4, + ), + min: _minRange, + max: _maxRange, + pointer: GaugePointer.circle( + position: const GaugePointerPosition.surface(), + radius: MediaQuery.sizeOf(context).width * 0.004, + color: ColorsManager.whiteColors, + border: GaugePointerBorder( + width: 6, + color: statusColor, + ), + shadow: const BoxShadow( + color: ColorsManager.blackColor, + blurRadius: 6, + offset: Offset(0, 2), + ), + ), + segments: const [ + GaugeSegment( + from: _minRange, + to: _goodRange, + cornerRadius: Radius.circular(16), + color: ColorsManager.goodGreen, + ), + GaugeSegment( + from: _goodRange + 1, + to: _moderateRange, + cornerRadius: Radius.circular(16), + color: ColorsManager.moderateYellow, + ), + GaugeSegment( + from: _moderateRange + 1, + to: _maxRange, + cornerRadius: Radius.circular(16), + color: ColorsManager.poorOrange, + ), + ], + ), + ); + } + + (String status, Color color) _getStatusData(double value) { + return switch (value) { + <= _goodRange => ('Good', ColorsManager.goodGreen), + <= _moderateRange => ('Moderate', ColorsManager.moderateYellow), + _ => ('Poor', ColorsManager.poorOrange), + }; + } + + Color _darkenColor(Color color) { + final black = Colors.black.withValues(alpha: 0.8); + return Color.lerp(color, black, 0.4)!; + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart new file mode 100644 index 00000000..7726ff32 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_humidity_and_temperature.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AqiHumidityAndTemperature extends StatelessWidget { + const AqiHumidityAndTemperature({ + required this.temperature, + required this.humidity, + super.key, + }); + + final int temperature; + final int humidity; + + static const iconSize = 12.0; + static const colorFilter = ColorFilter.mode( + ColorsManager.textPrimaryColor, + BlendMode.srcIn, + ); + + @override + Widget build(BuildContext context) { + return FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: DefaultTextStyle( + style: context.textTheme.bodySmall!.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 16, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildIconAndValue(Assets.temperatureAqiSidebar, '$temperature°C'), + const SizedBox(height: 10), + _buildIconAndValue(Assets.humidityAqiSidebar, '$humidity%'), + ], + ), + ), + ); + } + + Widget _buildIconAndValue(String icon, String value) { + return Row( + children: [ + SvgPicture.asset( + icon, + height: iconSize, + width: iconSize, + colorFilter: colorFilter, + ), + const SizedBox(width: 4), + Text(value), + ], + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart new file mode 100644 index 00000000..2503874f --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AqiLocation extends StatelessWidget { + const AqiLocation({ + required this.city, + required this.country, + required this.address, + super.key, + }); + + final String? city; + final String? country; + final String? address; + + String _getFormattedLocation() { + if (city == null && country == null && address == null) { + return 'N/A'; + } + + final parts = []; + + if (city != null) parts.add(city!); + if (address != null) parts.add(address!); + final locationPart = parts.join(', '); + + if (country != null) { + return locationPart.isEmpty ? country! : '$locationPart - $country'; + } + + return locationPart; + } + + @override + Widget build(BuildContext context) { + return Container( + decoration: subSectionContainerDecoration.copyWith( + boxShadow: const [], + borderRadius: BorderRadius.circular(10), + ), + padding: const EdgeInsetsDirectional.all(10), + child: Row( + spacing: 10, + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + _buildLocationPin(), + Expanded( + child: Text( + _getFormattedLocation(), + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + ), + ], + ), + ); + } + + Widget _buildLocationPin() { + return SvgPicture.asset( + Assets.locationPin, + height: 12, + width: 12, + alignment: AlignmentDirectional.centerStart, + colorFilter: const ColorFilter.mode( + ColorsManager.vividBlue, + BlendMode.srcIn, + ), + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart new file mode 100644 index 00000000..983f76b2 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info.dart @@ -0,0 +1,56 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class AqiLocationInfo extends StatelessWidget { + const AqiLocationInfo({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final info = state.locationInfo; + return Container( + decoration: secondarySection.copyWith(boxShadow: const []), + padding: const EdgeInsetsDirectional.all(20), + child: Column( + spacing: 8, + children: [ + AqiLocation( + city: info?.city, + country: info?.country, + address: info?.address, + ), + Expanded( + child: Row( + spacing: 8, + children: [ + AqiLocationInfoCell( + label: 'Temperature', + value: ' ${info?.temperature?.roundToDouble() ?? '--'}°', + svgPath: Assets.aqiTemperature, + ), + AqiLocationInfoCell( + label: 'Humidity', + value: '${info?.humidity?.roundToDouble() ?? '--'}%', + svgPath: Assets.aqiHumidity, + ), + AqiLocationInfoCell( + label: 'Air Quality', + value: ' ${info?.airQuality?.roundToDouble() ?? '--'}', + svgPath: Assets.aqiAirQuality, + ), + ], + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart new file mode 100644 index 00000000..fa0216a1 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_location_info_cell.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AqiLocationInfoCell extends StatelessWidget { + const AqiLocationInfoCell({ + required this.label, + required this.value, + required this.svgPath, + super.key, + }); + + final String label; + final String value; + final String svgPath; + + @override + Widget build(BuildContext context) { + return Expanded( + child: Container( + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(12), + ), + child: Stack( + children: [ + Align( + alignment: AlignmentDirectional.topStart, + child: Padding( + padding: const EdgeInsetsDirectional.all(10), + child: SizedBox( + height: 24, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.topStart, + child: Text( + label, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + ), + ), + ), + ), + Align( + alignment: AlignmentDirectional.bottomEnd, + child: Padding( + padding: const EdgeInsetsDirectional.all(10), + child: SizedBox( + height: 40, + width: 120, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomEnd, + child: Text( + value, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.vividBlue.withValues(alpha: 0.7), + fontWeight: FontWeight.w700, + fontSize: 24, + ), + ), + ), + ), + ), + ), + Align( + alignment: AlignmentDirectional.bottomStart, + child: SizedBox.square( + dimension: MediaQuery.sizeOf(context).width * 0.45, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.bottomStart, + child: SvgPicture.asset(svgPath), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart new file mode 100644 index 00000000..5a8e6e6c --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_sub_value_widget.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +final class _AqiRange { + const _AqiRange({required this.max, required this.color}); + + final double max; + final Color color; +} + +class AqiSubValueWidget extends StatelessWidget { + const AqiSubValueWidget({ + required this.label, + required this.value, + required this.unit, + required this.range, + super.key, + }); + + final String label; + final String value; + final String unit; + final (double min, double max) range; + + double get _parsedValue => double.parse(value); + + static const List<_AqiRange> _ranges = [ + _AqiRange(max: 12, color: ColorsManager.goodGreen), + _AqiRange(max: 35, color: ColorsManager.poorOrange), + _AqiRange(max: 55, color: ColorsManager.poorOrange), + _AqiRange(max: 150, color: ColorsManager.unhealthyRed), + _AqiRange(max: 250, color: ColorsManager.severePink), + _AqiRange(max: 500, color: ColorsManager.hazardousPurple), + ]; + + static List<_AqiRange> _getRangesForValue((double min, double max) range) { + final (double min, double max) = range; + final rangeSize = (max - min) / 6; + return [ + _AqiRange(max: min + rangeSize, color: ColorsManager.goodGreen), + _AqiRange(max: min + (rangeSize * 2), color: ColorsManager.poorOrange), + _AqiRange(max: min + (rangeSize * 3), color: ColorsManager.poorOrange), + _AqiRange(max: min + (rangeSize * 4), color: ColorsManager.unhealthyRed), + _AqiRange(max: min + (rangeSize * 5), color: ColorsManager.severePink), + _AqiRange(max: min, color: ColorsManager.hazardousPurple), + ]; + } + + int _getActiveSegmentByRange(double value, (double min, double max) range) { + final ranges = _getRangesForValue(range); + for (int i = 0; i < ranges.length; i++) { + if (value <= ranges[i].max) return i; + } + return ranges.length - 1; + } + + @override + Widget build(BuildContext context) { + final activeSegment = _getActiveSegmentByRange(_parsedValue, range); + return Expanded( + child: Container( + padding: const EdgeInsetsDirectional.all(10), + decoration: BoxDecoration( + color: ColorsManager.whiteColors, + borderRadius: BorderRadius.circular(12), + ), + child: Row( + spacing: MediaQuery.sizeOf(context).width * 0.0075, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildLabel(context), + _buildSegmentedBar(activeSegment), + _buildValueAndUnit(context), + ], + ), + ), + ); + } + + Widget _buildValueAndUnit(BuildContext context) { + return Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerEnd, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + value, + style: context.textTheme.titleMedium?.copyWith( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + Text( + unit, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 10, + ), + ), + ], + ), + ), + ); + } + + Widget _buildSegmentedBar(int activeSegment) { + return Expanded( + flex: 4, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 12), + child: Row( + spacing: 4, + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(_ranges.length, (index) { + final isActive = index == activeSegment; + final color = _ranges[index].color.withValues( + alpha: isActive ? 1.0 : 0.25, + ); + return Expanded( + child: AnimatedContainer( + duration: const Duration(milliseconds: 150), + curve: Curves.linear, + height: 5, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(4), + ), + ), + ); + }), + ), + ), + ); + } + + Widget _buildLabel(BuildContext context) { + return Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: Text( + label, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + ), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart new file mode 100644 index 00000000..5d482d9c --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +enum AqiType { + aqi('AQI', '', 'aqi'), + pm25('PM2.5', 'µg/m³', 'pm25'), + pm10('PM10', 'µg/m³', 'pm10'), + hcho('HCHO', 'mg/m³', 'cho2'), + tvoc('TVOC', 'µg/m³', 'voc'), + co2('CO2', 'ppm', 'co2'); + + const AqiType(this.value, this.unit, this.code); + + final String value; + final String unit; + final String code; +} + +class AqiTypeDropdown extends StatefulWidget { + const AqiTypeDropdown({super.key, required this.onChanged}); + + final ValueChanged onChanged; + + @override + State createState() => _AqiTypeDropdownState(); +} + +class _AqiTypeDropdownState extends State { + AqiType? _selectedItem = AqiType.aqi; + + void _updateSelectedItem(AqiType? item) => setState(() => _selectedItem = item); + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: ColorsManager.greyColor, + width: 1, + ), + ), + child: DropdownButton( + value: _selectedItem, + isDense: true, + borderRadius: BorderRadius.circular(16), + dropdownColor: ColorsManager.whiteColors, + underline: const SizedBox.shrink(), + icon: const RotatedBox( + quarterTurns: 1, + child: Icon(Icons.chevron_right, size: 24), + ), + style: _getTextStyle(context), + padding: const EdgeInsetsDirectional.symmetric( + horizontal: 12, + vertical: 2, + ), + items: AqiType.values + .map((e) => DropdownMenuItem(value: e, child: Text(e.value))) + .toList(), + onChanged: (value) { + _updateSelectedItem(value); + widget.onChanged(value); + }, + ), + ); + } + + TextStyle? _getTextStyle(BuildContext context) { + return context.textTheme.labelSmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w700, + fontSize: 12, + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart new file mode 100644 index 00000000..5e731d90 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart @@ -0,0 +1,111 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/range_of_aqi_charts_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class RangeOfAqiChart extends StatelessWidget { + final List chartData; + + const RangeOfAqiChart({ + super.key, + required this.chartData, + }); + + List<(List values, Color color, Color? dotColor)> get _lines { + final sortedData = List.from(chartData) + ..sort((a, b) => a.date.compareTo(b.date)); + + return [ + ( + sortedData.map((e) { + final value = e.data.firstOrNull; + return value?.max ?? 0; + }).toList(), + ColorsManager.maxPurple, + ColorsManager.maxPurpleDot, + ), + ( + sortedData.map((e) { + final value = e.data.firstOrNull; + return value?.average ?? 0; + }).toList(), + Colors.white, + null, + ), + ( + sortedData.map((e) { + final value = e.data.firstOrNull; + return value?.min ?? 0; + }).toList(), + ColorsManager.minBlue, + ColorsManager.minBlueDot, + ), + ]; + } + + @override + Widget build(BuildContext context) { + return LineChart( + LineChartData( + minY: 0, + maxY: 301, + clipData: const FlClipData.vertical(), + gridData: EnergyManagementChartsHelper.gridData(horizontalInterval: 50), + titlesData: RangeOfAqiChartsHelper.titlesData(context, chartData), + borderData: EnergyManagementChartsHelper.borderData(), + lineTouchData: RangeOfAqiChartsHelper.lineTouchData(chartData), + betweenBarsData: [ + BetweenBarsData( + fromIndex: 0, + toIndex: 2, + gradient: LinearGradient( + begin: Alignment.bottomCenter, + end: Alignment.topCenter, + stops: const [0.0, 0.2, 0.4, 0.6, 0.8, 1.0], + colors: RangeOfAqiChartsHelper.gradientData.map((e) { + final (color, _) = e; + return color.withValues(alpha: 0.6); + }).toList(), + ), + ), + ], + lineBarsData: _lines.map((e) { + final (values, color, dotColor) = e; + return _buildLine(values: values, color: color, dotColor: dotColor); + }).toList(), + ), + duration: Duration.zero, + ); + } + + FlDotData _buildDotData(Color color) { + return FlDotData( + show: true, + getDotPainter: (_, __, ___, ____) => FlDotCirclePainter( + radius: 2, + color: ColorsManager.whiteColors, + strokeWidth: 2, + strokeColor: color, + ), + ); + } + + LineChartBarData _buildLine({ + required List values, + required Color color, + Color? dotColor, + }) { + const invisibleDot = FlDotData(show: false); + return LineChartBarData( + spots: List.generate(values.length, (i) => FlSpot(i.toDouble(), values[i])), + isCurved: true, + color: color, + barWidth: 4, + isStrokeCapRound: true, + dotData: dotColor != null ? _buildDotData(dotColor) : invisibleDot, + belowBarData: BarAreaData(show: false), + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart new file mode 100644 index 00000000..6548c696 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_box.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class RangeOfAqiChartBox extends StatelessWidget { + const RangeOfAqiChartBox({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Container( + padding: const EdgeInsetsDirectional.all(30), + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(30), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (state.errorMessage != null) ...[ + AnalyticsErrorWidget(state.errorMessage), + const SizedBox(height: 10), + ], + RangeOfAqiChartTitle( + isLoading: state.status == RangeOfAqiStatus.loading, + ), + const SizedBox(height: 10), + const Divider(), + const SizedBox(height: 20), + Expanded(child: RangeOfAqiChart(chartData: state.filteredRangeOfAqi)), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart new file mode 100644 index 00000000..1b0da288 --- /dev/null +++ b/lib/pages/analytics/modules/air_quality/widgets/range_of_aqi_chart_title.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; +import 'package:syncrow_web/pages/analytics/widgets/charts_loading_widget.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; + +class RangeOfAqiChartTitle extends StatelessWidget { + const RangeOfAqiChartTitle({ + required this.isLoading, + super.key, + }); + + final bool isLoading; + + static const List<(Color color, String title, bool hasBorder)> _colors = [ + (Color(0xFF962DFF), 'Max', false), + (Color(0xFF93AAFD), 'Min', false), + (Colors.transparent, 'Avg', true), + ]; + + @override + Widget build(BuildContext context) { + return Row( + children: [ + ChartsLoadingWidget(isLoading: isLoading), + const Expanded( + flex: 3, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: ChartTitle(title: Text('Range of AQI')), + ), + ), + const Spacer(flex: 3), + ..._colors.map( + (e) { + final (color, title, hasBorder) = e; + return Expanded( + child: IntrinsicHeight( + child: FittedBox( + fit: BoxFit.fitWidth, + alignment: AlignmentDirectional.centerStart, + child: Padding( + padding: const EdgeInsetsDirectional.only(end: 16), + child: ChartInformativeCell( + title: Text(title), + color: color, + hasBorder: hasBorder, + ), + ), + ), + ), + ); + }, + ), + const Spacer(), + Expanded( + flex: 2, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerEnd, + child: AqiTypeDropdown( + onChanged: (value) { + final spaceTreeState = context.read().state; + final spaceUuid = spaceTreeState.selectedSpaces.firstOrNull; + + if (spaceUuid == null) return; + + if (value != null) { + context.read().add(UpdateAqiTypeEvent(value)); + } + }, + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart b/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart index b848f79f..fa170cd0 100644 --- a/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart +++ b/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart @@ -2,16 +2,23 @@ import 'package:bloc/bloc.dart'; import 'package:equatable/equatable.dart'; part 'analytics_date_picker_event.dart'; +part 'analytics_date_picker_state.dart'; -class AnalyticsDatePickerBloc extends Bloc { - AnalyticsDatePickerBloc() : super(DateTime.now()) { +class AnalyticsDatePickerBloc + extends Bloc { + AnalyticsDatePickerBloc() : super(AnalyticsDatePickerState()) { on(_onUpdateAnalyticsDatePickerEvent); } void _onUpdateAnalyticsDatePickerEvent( UpdateAnalyticsDatePickerEvent event, - Emitter emit, + Emitter emit, ) { - emit(event.date); + emit( + state.copyWith( + monthlyDate: event.montlyDate ?? state.monthlyDate, + yearlyDate: event.yearlyDate ?? state.yearlyDate, + ), + ); } } diff --git a/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_event.dart b/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_event.dart index 4fdb265e..6153aca1 100644 --- a/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_event.dart +++ b/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_event.dart @@ -8,10 +8,11 @@ sealed class AnalyticsDatePickerEvent extends Equatable { } final class UpdateAnalyticsDatePickerEvent extends AnalyticsDatePickerEvent { - const UpdateAnalyticsDatePickerEvent(this.date); + const UpdateAnalyticsDatePickerEvent({this.montlyDate, this.yearlyDate}); - final DateTime date; + final DateTime? montlyDate; + final DateTime? yearlyDate; @override - List get props => [date]; + List get props => [montlyDate, yearlyDate]; } diff --git a/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_state.dart b/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_state.dart new file mode 100644 index 00000000..ffcd4e40 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_state.dart @@ -0,0 +1,25 @@ +part of 'analytics_date_picker_bloc.dart'; + +final class AnalyticsDatePickerState extends Equatable { + AnalyticsDatePickerState({ + DateTime? monthlyDate, + DateTime? yearlyDate, + }) : monthlyDate = monthlyDate ?? DateTime.now(), + yearlyDate = yearlyDate ?? DateTime.now(); + + final DateTime monthlyDate; + final DateTime yearlyDate; + + AnalyticsDatePickerState copyWith({ + DateTime? monthlyDate, + DateTime? yearlyDate, + }) { + return AnalyticsDatePickerState( + monthlyDate: monthlyDate ?? this.monthlyDate, + yearlyDate: yearlyDate ?? this.yearlyDate, + ); + } + + @override + List get props => [monthlyDate, yearlyDate]; +} diff --git a/lib/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart b/lib/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart new file mode 100644 index 00000000..fbd28dee --- /dev/null +++ b/lib/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart @@ -0,0 +1,77 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; +import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; +import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; + +part 'analytics_devices_event.dart'; +part 'analytics_devices_state.dart'; + +class AnalyticsDevicesBloc + extends Bloc { + AnalyticsDevicesBloc( + this._analyticsDevicesService, + ) : super(const AnalyticsDevicesState()) { + on(_onLoadAnalyticsDevices); + on(_onSelectAnalyticsDevice); + on(_onClearAnalyticsDevice); + } + final AnalyticsDevicesService _analyticsDevicesService; + + Future _onLoadAnalyticsDevices( + LoadAnalyticsDevicesEvent event, + Emitter emit, + ) async { + emit(const AnalyticsDevicesState(status: AnalyticsDevicesStatus.loading)); + + try { + final devices = await _analyticsDevicesService.getDevices(event.param); + emit( + AnalyticsDevicesState( + status: AnalyticsDevicesStatus.loaded, + devices: devices, + selectedDevice: devices.firstOrNull, + ), + ); + if (devices.isNotEmpty) { + event.onSuccess(devices.first); + } + } on APIException catch (e) { + emit( + AnalyticsDevicesState( + status: AnalyticsDevicesStatus.failure, + errorMessage: e.message, + ), + ); + } catch (e) { + emit( + AnalyticsDevicesState( + status: AnalyticsDevicesStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + void _onSelectAnalyticsDevice( + SelectAnalyticsDeviceEvent event, + Emitter emit, + ) { + emit( + AnalyticsDevicesState( + selectedDevice: event.device, + devices: state.devices, + errorMessage: state.errorMessage, + status: state.status, + ), + ); + } + + void _onClearAnalyticsDevice( + ClearAnalyticsDeviceEvent event, + Emitter emit, + ) { + emit(const AnalyticsDevicesState()); + } +} diff --git a/lib/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_event.dart b/lib/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_event.dart new file mode 100644 index 00000000..fb61e73b --- /dev/null +++ b/lib/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_event.dart @@ -0,0 +1,31 @@ +part of 'analytics_devices_bloc.dart'; + +sealed class AnalyticsDevicesEvent extends Equatable { + const AnalyticsDevicesEvent(); + + @override + List get props => []; +} + +final class LoadAnalyticsDevicesEvent extends AnalyticsDevicesEvent { + const LoadAnalyticsDevicesEvent({required this.param, required this.onSuccess}); + + final GetAnalyticsDevicesParam param; + final void Function(AnalyticsDevice device) onSuccess; + + @override + List get props => [param]; +} + +final class SelectAnalyticsDeviceEvent extends AnalyticsDevicesEvent { + const SelectAnalyticsDeviceEvent(this.device); + + final AnalyticsDevice device; + + @override + List get props => [device]; +} + +final class ClearAnalyticsDeviceEvent extends AnalyticsDevicesEvent { + const ClearAnalyticsDeviceEvent(); +} diff --git a/lib/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_state.dart b/lib/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_state.dart new file mode 100644 index 00000000..7c1be359 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_state.dart @@ -0,0 +1,20 @@ +part of 'analytics_devices_bloc.dart'; + +enum AnalyticsDevicesStatus { initial, loading, loaded, failure } + +final class AnalyticsDevicesState extends Equatable { + const AnalyticsDevicesState({ + this.status = AnalyticsDevicesStatus.initial, + this.devices = const [], + this.errorMessage, + this.selectedDevice, + }); + + final AnalyticsDevicesStatus status; + final List devices; + final AnalyticsDevice? selectedDevice; + final String? errorMessage; + + @override + List get props => [status, devices, errorMessage, selectedDevice]; +} diff --git a/lib/pages/analytics/modules/analytics/enums/analytics_page_tab.dart b/lib/pages/analytics/modules/analytics/enums/analytics_page_tab.dart index b26cfc95..6552f6cf 100644 --- a/lib/pages/analytics/modules/analytics/enums/analytics_page_tab.dart +++ b/lib/pages/analytics/modules/analytics/enums/analytics_page_tab.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/views/air_quality_view.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart'; import 'package:syncrow_web/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart'; @@ -10,6 +11,10 @@ enum AnalyticsPageTab { occupancy( title: 'Occupancy', child: AnalyticsOccupancyView(), + ), + airQuality( + title: 'Air Quality', + child: AirQualityView(), ); const AnalyticsPageTab({ diff --git a/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart new file mode 100644 index 00000000..8b1802af --- /dev/null +++ b/lib/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; + +final class AirQualityDataLoadingStrategy implements AnalyticsDataLoadingStrategy { + @override + void onCommunitySelected( + BuildContext context, + CommunityModel community, + List spaces, + ) { + // Do nothing + } + + @override + void onSpaceSelected( + BuildContext context, + CommunityModel community, + SpaceModel space, + ) { + final spaceTreeBloc = context.read(); + final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid); + + final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty; + if (hasSelectedSpaces) clearData(context); + + if (isSpaceSelected) return; + + spaceTreeBloc + ..add(const SpaceTreeClearSelectionEvent()) + ..add(OnSpaceSelected(community, space.uuid ?? '', [])); + + FetchAirQualityDataHelper.loadAirQualityData( + context, + communityUuid: community.uuid, + spaceUuid: space.uuid ?? '', + date: context.read().state.monthlyDate, + ); + } + + @override + void clearData(BuildContext context) { + context.read().add( + const AnalyticsClearAllSpaceTreeSelectionsEvent(), + ); + FetchAirQualityDataHelper.clearAllData(context); + } +} diff --git a/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart new file mode 100644 index 00000000..654455b2 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart @@ -0,0 +1,17 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; + +abstract class AnalyticsDataLoadingStrategy { + void onCommunitySelected( + BuildContext context, + CommunityModel community, + List spaces, + ); + void onSpaceSelected( + BuildContext context, + CommunityModel community, + SpaceModel space, + ); + void clearData(BuildContext context); +} diff --git a/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart b/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart new file mode 100644 index 00000000..19b0aff2 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart @@ -0,0 +1,16 @@ +import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/air_quality_data_loading_strategy.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart'; + +abstract final class AnalyticsDataLoadingStrategyFactory { + const AnalyticsDataLoadingStrategyFactory._(); + static AnalyticsDataLoadingStrategy getStrategy(AnalyticsPageTab tab) { + return switch (tab) { + AnalyticsPageTab.energyManagement => EnergyManagementDataLoadingStrategy(), + AnalyticsPageTab.occupancy => OccupancyDataLoadingStrategy(), + AnalyticsPageTab.airQuality => AirQualityDataLoadingStrategy(), + }; + } +} diff --git a/lib/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart new file mode 100644 index 00000000..757b2a9a --- /dev/null +++ b/lib/pages/analytics/modules/analytics/strategies/energy_management_data_loading_strategy.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; + +class EnergyManagementDataLoadingStrategy implements AnalyticsDataLoadingStrategy { + @override + void onCommunitySelected( + BuildContext context, + CommunityModel community, + List spaces, + ) { + final spaceTreeBloc = context.read(); + final isCommunitySelected = + spaceTreeBloc.state.selectedCommunities.contains(community.uuid); + + if (isCommunitySelected) { + clearData(context); + return; + } + } + + @override + void onSpaceSelected( + BuildContext context, + CommunityModel community, + SpaceModel space, + ) { + final spaceTreeBloc = context.read(); + final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid); + final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty; + + if (isSpaceSelected) { + final firstSelectedSpace = spaceTreeBloc.state.selectedSpaces.first; + final isTheFirstSelectedSpace = firstSelectedSpace == space.uuid; + if (isTheFirstSelectedSpace) { + clearData(context); + } + return; + } + + if (hasSelectedSpaces) { + clearData(context); + } + + spaceTreeBloc.add( + OnSpaceSelected( + community, + space.uuid ?? '', + space.children, + ), + ); + + FetchEnergyManagementDataHelper.loadEnergyManagementData( + context, + communityId: community.uuid, + spaceId: space.uuid ?? '', + ); + } + + @override + void clearData(BuildContext context) { + context.read().add( + const AnalyticsClearAllSpaceTreeSelectionsEvent(), + ); + FetchEnergyManagementDataHelper.clearAllData(context); + } +} diff --git a/lib/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart b/lib/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart new file mode 100644 index 00000000..9bffe3b4 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/strategies/occupancy_data_loading_strategy.dart @@ -0,0 +1,52 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; + +class OccupancyDataLoadingStrategy implements AnalyticsDataLoadingStrategy { + @override + void onCommunitySelected( + BuildContext context, + CommunityModel community, + List spaces, + ) { + // Do Nothing + } + + @override + void onSpaceSelected( + BuildContext context, + CommunityModel community, + SpaceModel space, + ) { + final spaceTreeBloc = context.read(); + final isSpaceSelected = spaceTreeBloc.state.selectedSpaces.contains(space.uuid); + + final hasSelectedSpaces = spaceTreeBloc.state.selectedSpaces.isNotEmpty; + if (hasSelectedSpaces) clearData(context); + + if (isSpaceSelected) return; + + spaceTreeBloc + ..add(const SpaceTreeClearSelectionEvent()) + ..add(OnSpaceSelected(community, space.uuid ?? '', [])); + + FetchOccupancyDataHelper.loadOccupancyData( + context, + communityId: community.uuid, + spaceId: space.uuid ?? '', + ); + } + + @override + void clearData(BuildContext context) { + context.read().add( + const AnalyticsClearAllSpaceTreeSelectionsEvent(), + ); + FetchOccupancyDataHelper.clearAllData(context); + } +} diff --git a/lib/pages/analytics/modules/analytics/views/analytics_page.dart b/lib/pages/analytics/modules/analytics/views/analytics_page.dart index dbbf855f..3d8b1eb3 100644 --- a/lib/pages/analytics/modules/analytics/views/analytics_page.dart +++ b/lib/pages/analytics/modules/analytics/views/analytics_page.dart @@ -1,5 +1,11 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/air_quality_distribution/air_quality_distribution_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/device_location/device_location_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/blocs/range_of_aqi/range_of_aqi_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart'; @@ -8,19 +14,43 @@ import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/ener import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart'; -import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/fake_energy_consumption_by_phases_service.dart'; -import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/fake_energy_consumption_per_device_service.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart'; +import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart'; +import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart'; +import 'package:syncrow_web/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart'; +import 'package:syncrow_web/pages/analytics/services/device_location/device_location_details_service_decorator.dart'; +import 'package:syncrow_web/pages/analytics/services/device_location/remote_device_location_service.dart'; +import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart'; +import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart'; +import 'package:syncrow_web/pages/analytics/services/occupacy/remote_occupancy_service.dart'; +import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart'; import 'package:syncrow_web/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart'; +import 'package:syncrow_web/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart'; import 'package:syncrow_web/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart'; -import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/fake_total_energy_consumption_service.dart'; +import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart'; import 'package:syncrow_web/pages/device_managment/shared/navigate_home_grid_view.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/theme/responsive_text_theme.dart'; import 'package:syncrow_web/web_layout/web_scaffold.dart'; -class AnalyticsPage extends StatelessWidget { +class AnalyticsPage extends StatefulWidget { const AnalyticsPage({super.key}); + @override + State createState() => _AnalyticsPageState(); +} + +class _AnalyticsPageState extends State { + late final HTTPService _httpService; + + @override + void initState() { + super.initState(); + _httpService = HTTPService(); + } + @override Widget build(BuildContext context) { return MultiBlocProvider( @@ -30,22 +60,22 @@ class AnalyticsPage extends StatelessWidget { ), BlocProvider( create: (context) => TotalEnergyConsumptionBloc( - FakeTotalEnergyConsumptionService(), + RemoteTotalEnergyConsumptionService(_httpService), ), ), BlocProvider( create: (context) => EnergyConsumptionByPhasesBloc( - FakeEnergyConsumptionByPhasesService(), + RemoteEnergyConsumptionByPhasesService(_httpService), ), ), BlocProvider( create: (context) => EnergyConsumptionPerDeviceBloc( - FakeEnergyConsumptionPerDeviceService(), + RemoteEnergyConsumptionPerDeviceService(_httpService), ), ), BlocProvider( create: (context) => PowerClampInfoBloc( - RemotePowerClampInfoService(HTTPService()), + RemotePowerClampInfoService(_httpService), ), ), BlocProvider( @@ -53,6 +83,47 @@ class AnalyticsPage extends StatelessWidget { FirebaseRealtimeDeviceService(), ), ), + BlocProvider( + create: (context) => OccupancyBloc( + RemoteOccupancyService(_httpService), + ), + ), + BlocProvider( + create: (context) => OccupancyHeatMapBloc( + RemoteOccupancyHeatMapService(_httpService), + ), + ), + BlocProvider(create: (context) => AnalyticsDatePickerBloc()), + BlocProvider( + create: (context) => AnalyticsDevicesBloc( + AnalyticsDevicesServiceDelegate( + RemoteOccupancyAnalyticsDevicesService(_httpService), + RemoteEnergyManagementAnalyticsDevicesService(_httpService), + ), + ), + ), + BlocProvider( + create: (context) => RangeOfAqiBloc( + RemoteRangeOfAqiService(_httpService), + ), + ), + BlocProvider( + create: (context) => AirQualityDistributionBloc( + RemoteAirQualityDistributionService(_httpService), + ), + ), + BlocProvider( + create: (context) => DeviceLocationBloc( + DeviceLocationDetailsServiceDecorator( + RemoteDeviceLocationService(_httpService), + Dio( + BaseOptions( + baseUrl: 'https://nominatim.openstreetmap.org/', + ), + ), + ), + ), + ), ], child: const AnalyticsPageForm(), ); diff --git a/lib/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart b/lib/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart index 801af744..ab07737a 100644 --- a/lib/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart +++ b/lib/pages/analytics/modules/analytics/widgets/analytics_communities_sidebar.dart @@ -1,55 +1,29 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart'; -import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart'; -import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/sidebar/analytics_space_tree_view.dart'; class AnalyticsCommunitiesSidebar extends StatelessWidget { const AnalyticsCommunitiesSidebar({super.key}); @override Widget build(BuildContext context) { - return Builder( - builder: (context) { - return Expanded( - child: SpaceTreeView( - title: const Text('Communities'), - shouldDisableDeselectingChildrenOfSelectedParent: true, - onSelect: () { - /// Necessary to wait for the state to update before fethcing the data. - Future.delayed( - const Duration(milliseconds: 100), - () { - if (context.mounted) { - FetchEnergyManagementDataHelper.fetchEnergyManagementData( - context, - ); - FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges( - context, - ); - context.read().add( - const ClearPowerClampInfoEvent(), - ); - final (selectedCommunities, selectedSpaces) = - FetchEnergyManagementDataHelper - .getSelectedCommunitiesAndSpaces(context); - if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) { - context.read().add( - const ClearPowerClampInfoEvent(), - ); - } else { - FetchEnergyManagementDataHelper.loadPowerClampInfo( - context, - ); - } - } - }, - ); - }, - isSide: false, - ), - ); - }, + final selectedTab = context.watch().state; + final strategy = AnalyticsDataLoadingStrategyFactory.getStrategy(selectedTab); + + return Expanded( + child: AnalyticsSpaceTreeView( + onSelectCommunity: (community, spaces) { + strategy.onCommunitySelected(context, community, spaces); + }, + onSelectSpace: (community, space) { + strategy.onSpaceSelected(context, community, space); + }, + onSelectChildSpace: (community, child) { + strategy.onSpaceSelected(context, community, child); + }, + ), ); } } diff --git a/lib/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart b/lib/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart index 19a72566..616688dd 100644 --- a/lib/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart +++ b/lib/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart @@ -1,17 +1,26 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_svg/svg.dart'; import 'package:intl/intl.dart'; -import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/month_picker_widget.dart'; -import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/year_picker_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -class AnalyticsDateFilterButton extends StatefulWidget { - const AnalyticsDateFilterButton({super.key}); +enum DatePickerType { month, year } - static final _color = ColorsManager.blackColor.withValues(alpha: 0.8); +class AnalyticsDateFilterButton extends StatefulWidget { + const AnalyticsDateFilterButton({ + required this.selectedDate, + required this.onDateSelected, + this.datePickerType = DatePickerType.month, + super.key, + }); + + final DateTime selectedDate; + final void Function(DateTime)? onDateSelected; + final DatePickerType datePickerType; + + static final Color _color = ColorsManager.blackColor.withValues(alpha: 0.8); @override State createState() => @@ -19,79 +28,67 @@ class AnalyticsDateFilterButton extends StatefulWidget { } class _AnalyticsDateFilterButtonState extends State { - late final AnalyticsDatePickerBloc _analyticsDatePickerBloc; - @override - void initState() { - _analyticsDatePickerBloc = AnalyticsDatePickerBloc(); - super.initState(); - } - - @override - void dispose() { - _analyticsDatePickerBloc.close(); - super.dispose(); - } - @override Widget build(BuildContext context) { - return BlocProvider.value( - value: _analyticsDatePickerBloc, - child: Builder(builder: (context) { - final selectedDate = context.watch().state; - return TextButton.icon( - style: TextButton.styleFrom( - foregroundColor: AnalyticsDateFilterButton._color, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: const BorderSide( - color: ColorsManager.greyColor, - width: 1, - ), - ), - backgroundColor: ColorsManager.transparentColor, - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, - ), + return TextButton.icon( + style: TextButton.styleFrom( + foregroundColor: AnalyticsDateFilterButton._color, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide( + color: ColorsManager.greyColor, + width: 1, ), - icon: SvgPicture.asset( - Assets.blankCalendar, - height: 20, - width: 20, - colorFilter: - ColorFilter.mode(AnalyticsDateFilterButton._color, BlendMode.srcIn), - ), - label: Text( - _formatDate(selectedDate), - style: const TextStyle( - fontWeight: FontWeight.w700, - ), - ), - onPressed: () { - showDialog( - context: context, - builder: (_) => MonthPickerWidget( - selectedDate: selectedDate, + ), + backgroundColor: ColorsManager.transparentColor, + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + ), + icon: SvgPicture.asset( + Assets.blankCalendar, + height: 20, + width: 20, + colorFilter: + ColorFilter.mode(AnalyticsDateFilterButton._color, BlendMode.srcIn), + ), + label: Text( + _formatDate(widget.selectedDate), + style: const TextStyle( + fontWeight: FontWeight.w700, + ), + ), + onPressed: () { + showDialog( + context: context, + builder: (_) => switch (widget.datePickerType) { + DatePickerType.month => MonthPickerWidget( + selectedDate: widget.selectedDate, onDateSelected: (value) { - _analyticsDatePickerBloc.add( - UpdateAnalyticsDatePickerEvent(value), - ); - FetchEnergyManagementDataHelper.fetchEnergyManagementData( - context, - selectedDate: value, - ); + widget.onDateSelected?.call(value); + }, + ), + DatePickerType.year => YearPickerWidget( + selectedDate: widget.selectedDate, + onDateSelected: (value) { + widget.onDateSelected?.call(value); }, ), - ); }, ); - }), + }, ); } String _formatDate(DateTime? date) { - final formatter = DateFormat('MMMM yyyy'); - final formattedDate = formatter.format(date ?? DateTime.now()); + final formatterBasedOnDatePickerType = switch (widget.datePickerType) { + DatePickerType.month => DateFormat('MMMM yyyy'), + DatePickerType.year => DateFormat('yyyy'), + }; + final formattedDate = formatterBasedOnDatePickerType.format( + date ?? DateTime.now(), + ); return formattedDate; } } diff --git a/lib/pages/analytics/modules/analytics/widgets/analytics_page_tab_button.dart b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tab_button.dart index fb0983fa..9ff98ef2 100644 --- a/lib/pages/analytics/modules/analytics/widgets/analytics_page_tab_button.dart +++ b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tab_button.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/strategies/analytics_data_loading_strategy_factory.dart'; import 'package:syncrow_web/utils/color_manager.dart'; class AnalyticsPageTabButton extends StatelessWidget { @@ -17,9 +18,12 @@ class AnalyticsPageTabButton extends StatelessWidget { @override Widget build(BuildContext context) { return TextButton( - onPressed: () => context.read().add( + onPressed: () { + AnalyticsDataLoadingStrategyFactory.getStrategy(tab).clearData(context); + context.read().add( UpdateAnalyticsTabEvent(tab), - ), + ); + }, child: Text( tab.title, textAlign: TextAlign.center, diff --git a/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart index d7e2cfef..13501c19 100644 --- a/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart +++ b/lib/pages/analytics/modules/analytics/widgets/analytics_page_tabs_and_children.dart @@ -1,9 +1,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/air_quality/helpers/fetch_air_quality_data_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_tab/analytics_tab_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/enums/analytics_page_tab.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart'; import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_page_tab_button.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/utils/style.dart'; class AnalyticsPageTabsAndChildren extends StatelessWidget { @@ -38,9 +42,7 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.center, children: [ ...AnalyticsPageTab.values.map( - (tab) => AnimatedSwitcher( - switchInCurve: Curves.easeIn, - duration: const Duration(milliseconds: 200), + (tab) => _buildAnimation( child: AnalyticsPageTabButton( key: ValueKey(selectedTab), tab: tab, @@ -53,12 +55,25 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget { ), ), const Spacer(), - const Expanded( - flex: 2, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.centerEnd, - child: AnalyticsDateFilterButton(), + Visibility( + key: ValueKey(selectedTab), + visible: selectedTab == AnalyticsPageTab.energyManagement || + selectedTab == AnalyticsPageTab.airQuality, + child: Expanded( + flex: 2, + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerEnd, + child: AnalyticsDateFilterButton( + onDateSelected: (value) { + _onDateChanged(context, value, selectedTab); + }, + selectedDate: context + .watch() + .state + .monthlyDate, + ), + ), ), ), ], @@ -67,14 +82,89 @@ class AnalyticsPageTabsAndChildren extends StatelessWidget { ), Expanded( flex: 8, - child: AnimatedSwitcher( - switchInCurve: Curves.easeIn, - duration: const Duration(milliseconds: 200), - child: selectedTab.child, - ), + child: _buildAnimation(child: selectedTab.child), ), ], ), ); } + + Widget _buildAnimation({required Widget child}) { + return AnimatedSwitcher( + switchInCurve: Curves.easeIn, + duration: const Duration(milliseconds: 200), + child: child, + ); + } + + void _onDateChanged( + BuildContext context, + DateTime date, + AnalyticsPageTab selectedTab, + ) { + context.read().add( + UpdateAnalyticsDatePickerEvent(montlyDate: date), + ); + + final spaceTreeState = context.read().state; + final communities = spaceTreeState.selectedCommunities; + final spaces = spaceTreeState.selectedSpaces; + if (spaceTreeState.selectedSpaces.isNotEmpty) { + switch (selectedTab) { + case AnalyticsPageTab.energyManagement: + _onEnergyManagementDateChanged( + context, + date: date, + communityUuid: communities.firstOrNull ?? '', + spaceUuid: spaces.firstOrNull ?? '', + ); + return; + case AnalyticsPageTab.airQuality: + _onAirQualityDateChanged( + context, + date: date, + communityUuid: communities.firstOrNull ?? '', + spaceUuid: spaces.firstOrNull ?? '', + ); + return; + default: + return; + } + } + } + + void _onEnergyManagementDateChanged( + BuildContext context, { + required DateTime date, + required String communityUuid, + required String spaceUuid, + }) { + context.read().add( + UpdateAnalyticsDatePickerEvent(montlyDate: date), + ); + + FetchEnergyManagementDataHelper.loadEnergyManagementData( + context, + shouldFetchAnalyticsDevices: false, + selectedDate: date, + communityId: communityUuid, + spaceId: spaceUuid, + ); + } + + void _onAirQualityDateChanged( + BuildContext context, { + required DateTime date, + required String communityUuid, + required String spaceUuid, + }) { + if (spaceUuid.isEmpty) return; + FetchAirQualityDataHelper.loadAirQualityData( + context, + date: date, + communityUuid: communityUuid, + spaceUuid: spaceUuid, + shouldFetchAnalyticsDevices: false, + ); + } } diff --git a/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart new file mode 100644 index 00000000..eec31998 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class ChartInformativeCell extends StatelessWidget { + const ChartInformativeCell({ + super.key, + required this.title, + required this.color, + this.hasBorder = false, + }); + + final Widget title; + final Color color; + final bool hasBorder; + + @override + Widget build(BuildContext context) { + return Container( + height: MediaQuery.sizeOf(context).height * 0.0385, + padding: const EdgeInsetsDirectional.symmetric( + vertical: 8, + horizontal: 12, + ), + decoration: BoxDecoration( + borderRadius: BorderRadiusDirectional.circular(8), + border: Border.all( + color: ColorsManager.greyColor, + width: 1, + ), + ), + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: Alignment.center, + child: Row( + spacing: 6, + children: [ + Container( + height: 8, + width: 8, + decoration: BoxDecoration( + color: color, + border: Border.all(color: ColorsManager.grayBorder), + shape: BoxShape.circle, + ), + ), + DefaultTextStyle( + style: const TextStyle( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 14, + ), + child: title, + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/analytics/widgets/month_picker_widget.dart b/lib/pages/analytics/modules/analytics/widgets/month_picker_widget.dart index 53ba31af..57133b02 100644 --- a/lib/pages/analytics/modules/analytics/widgets/month_picker_widget.dart +++ b/lib/pages/analytics/modules/analytics/widgets/month_picker_widget.dart @@ -45,7 +45,7 @@ class _MonthPickerWidgetState extends State { @override Widget build(BuildContext context) { return Dialog( - backgroundColor: Theme.of(context).colorScheme.surface, + backgroundColor: ColorsManager.whiteColors, child: Container( padding: const EdgeInsetsDirectional.all(20), width: 320, @@ -121,6 +121,7 @@ class _MonthPickerWidgetState extends State { } Row _buildYearSelector() { + final currentYear = DateTime.now().year; return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -134,17 +135,35 @@ class _MonthPickerWidgetState extends State { ), const Spacer(), IconButton( - onPressed: () => setState(() => _currentYear = _currentYear - 1), + onPressed: () { + setState(() { + _currentYear = _currentYear - 1; + }); + }, icon: const Icon( Icons.chevron_left, color: ColorsManager.grey700, ), ), IconButton( - onPressed: () => setState(() => _currentYear = _currentYear + 1), - icon: const Icon( + onPressed: _currentYear < currentYear + ? () { + setState(() { + _currentYear = _currentYear + 1; + // Clear selected month if it becomes invalid in the new year + if (_currentYear == currentYear && + _selectedMonth != null && + _selectedMonth! > DateTime.now().month - 1) { + _selectedMonth = null; + } + }); + } + : null, + icon: Icon( Icons.chevron_right, - color: ColorsManager.grey700, + color: _currentYear < currentYear + ? ColorsManager.grey700 + : ColorsManager.grey700.withValues(alpha: 0.3), ), ), ], @@ -152,11 +171,13 @@ class _MonthPickerWidgetState extends State { } Widget _buildMonthsGrid() { + final currentDate = DateTime.now(); + final isCurrentYear = _currentYear == currentDate.year; + return GridView.builder( shrinkWrap: true, itemCount: 12, physics: const NeverScrollableScrollPhysics(), - padding: const EdgeInsets.all(8), gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, childAspectRatio: 2.5, @@ -165,25 +186,43 @@ class _MonthPickerWidgetState extends State { ), itemBuilder: (context, index) { final isSelected = _selectedMonth == index; + final isFutureMonth = isCurrentYear && index > currentDate.month - 1; + return InkWell( - onTap: () => setState(() => _selectedMonth = index), - child: Container( - alignment: Alignment.center, + onTap: isFutureMonth ? null : () => setState(() => _selectedMonth = index), + child: DecoratedBox( decoration: BoxDecoration( - color: isSelected - ? ColorsManager.vividBlue.withValues(alpha: 0.7) - : const Color(0xFFEDF2F7), - borderRadius: - isSelected ? BorderRadius.circular(15) : BorderRadius.zero, + color: const Color(0xFFEDF2F7), + borderRadius: BorderRadius.only( + topLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero, + bottomLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero, + topRight: index % 3 == 2 ? const Radius.circular(16) : Radius.zero, + bottomRight: + index % 3 == 2 ? const Radius.circular(16) : Radius.zero, + ), ), - child: Text( - _monthNames[index], - style: context.textTheme.titleSmall?.copyWith( - fontSize: 12, + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( color: isSelected - ? ColorsManager.whiteColors - : ColorsManager.blackColor.withValues(alpha: 0.8), - fontWeight: FontWeight.w500, + ? ColorsManager.vividBlue.withValues(alpha: 0.7) + : isFutureMonth + ? ColorsManager.grey700.withValues(alpha: 0.1) + : const Color(0xFFEDF2F7), + borderRadius: + isSelected ? BorderRadius.circular(15) : BorderRadius.zero, + ), + child: Text( + _monthNames[index], + style: context.textTheme.titleSmall?.copyWith( + fontSize: 12, + color: isSelected + ? ColorsManager.whiteColors + : isFutureMonth + ? ColorsManager.blackColor.withValues(alpha: 0.3) + : ColorsManager.blackColor.withValues(alpha: 0.8), + fontWeight: FontWeight.w500, + ), ), ), ), diff --git a/lib/pages/analytics/modules/analytics/widgets/sidebar/analytics_space_tree_view.dart b/lib/pages/analytics/modules/analytics/widgets/sidebar/analytics_space_tree_view.dart new file mode 100644 index 00000000..f900a040 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/widgets/sidebar/analytics_space_tree_view.dart @@ -0,0 +1,188 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/common/widgets/search_bar.dart'; +import 'package:syncrow_web/common/widgets/sidebar_communities_list.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_state.dart'; +import 'package:syncrow_web/pages/space_tree/view/custom_expansion.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AnalyticsSpaceTreeView extends StatefulWidget { + const AnalyticsSpaceTreeView({ + super.key, + this.onSelectCommunity, + this.onSelectSpace, + this.onSelectChildSpace, + }); + + final void Function( + CommunityModel community, + List spaces, + )? onSelectCommunity; + final void Function( + CommunityModel community, + SpaceModel space, + )? onSelectSpace; + final void Function( + CommunityModel community, + SpaceModel child, + )? onSelectChildSpace; + + @override + State createState() => _AnalyticsSpaceTreeViewState(); +} + +class _AnalyticsSpaceTreeViewState extends State { + late final ScrollController _scrollController; + + @override + void initState() { + _scrollController = ScrollController(); + super.initState(); + } + + @override + void dispose() { + _scrollController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return BlocBuilder(builder: (context, state) { + final communities = state.searchQuery.isNotEmpty + ? state.filteredCommunity + : state.communityList; + return Container( + height: MediaQuery.sizeOf(context).height, + decoration: const BoxDecoration(color: ColorsManager.whiteColors), + child: state is SpaceTreeLoadingState + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + Container( + alignment: AlignmentDirectional.centerStart, + padding: const EdgeInsets.all(24), + child: DefaultTextStyle( + style: context.textTheme.titleMedium!.copyWith( + color: ColorsManager.blackColor, + fontSize: 20, + ), + child: const Text('Communities'), + ), + ), + CustomSearchBar( + onSearchChanged: (query) => context.read().add( + SearchQueryEvent(query), + ), + ), + const SizedBox(height: 16), + Expanded( + child: state.isSearching + ? const Center(child: CircularProgressIndicator()) + : SidebarCommunitiesList( + onScrollToEnd: () { + if (!state.paginationIsLoading) { + context.read().add( + PaginationEvent( + state.paginationModel, + state.communityList, + ), + ); + } + }, + scrollController: _scrollController, + communities: communities, + itemBuilder: (context, index) { + return CustomExpansionTileSpaceTree( + title: communities[index].name, + isSelected: state.selectedCommunities + .contains(communities[index].uuid), + isSoldCheck: state.selectedCommunities + .contains(communities[index].uuid), + onExpansionChanged: () => + context.read().add( + OnCommunityExpanded( + communities[index].uuid, + ), + ), + isExpanded: state.expandedCommunities.contains( + communities[index].uuid, + ), + onItemSelected: () => widget.onSelectCommunity?.call( + communities[index], + communities[index].spaces, + ), + children: communities[index].spaces.map( + (space) { + return CustomExpansionTileSpaceTree( + title: space.name, + isExpanded: + state.expandedSpaces.contains(space.uuid), + onItemSelected: () => + widget.onSelectSpace?.call( + communities[index], + space, + ), + onExpansionChanged: () => + context.read().add( + OnSpaceExpanded( + communities[index].uuid, + space.uuid ?? '', + ), + ), + isSelected: state.selectedSpaces + .contains(space.uuid) || + state.soldCheck.contains(space.uuid), + isSoldCheck: + state.soldCheck.contains(space.uuid), + children: _buildNestedSpaces( + context, + state, + space, + communities[index], + ), + ); + }, + ).toList(), + ); + }, + ), + ), + if (state.paginationIsLoading) const CircularProgressIndicator(), + ], + ), + ); + }); + } + + List _buildNestedSpaces( + BuildContext context, + SpaceTreeState state, + SpaceModel space, + CommunityModel community, + ) { + return space.children.map((child) { + return CustomExpansionTileSpaceTree( + isSelected: state.selectedSpaces.contains(child.uuid) || + state.soldCheck.contains(child.uuid), + isSoldCheck: state.soldCheck.contains(child.uuid), + title: child.name, + isExpanded: state.expandedSpaces.contains(child.uuid), + onItemSelected: () { + widget.onSelectChildSpace?.call(community, child); + }, + onExpansionChanged: () { + context.read().add( + OnSpaceExpanded(community.uuid, child.uuid ?? ''), + ); + }, + children: _buildNestedSpaces(context, state, child, community), + ); + }).toList(); + } +} diff --git a/lib/pages/analytics/modules/analytics/widgets/year_picker_widget.dart b/lib/pages/analytics/modules/analytics/widgets/year_picker_widget.dart new file mode 100644 index 00000000..22eb6646 --- /dev/null +++ b/lib/pages/analytics/modules/analytics/widgets/year_picker_widget.dart @@ -0,0 +1,158 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class YearPickerWidget extends StatefulWidget { + const YearPickerWidget({ + super.key, + required this.selectedDate, + required this.onDateSelected, + }); + + final DateTime selectedDate; + final ValueChanged? onDateSelected; + + @override + State createState() => _YearPickerWidgetState(); +} + +class _YearPickerWidgetState extends State { + late int _currentYear; + + static final years = List.generate( + DateTime.now().year - (DateTime.now().year - 5) + 1, + (index) => (2020 + index), + ).where((year) => year <= DateTime.now().year).toList(); + + @override + void initState() { + super.initState(); + _currentYear = widget.selectedDate.year; + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: ColorsManager.whiteColors, + child: Container( + padding: const EdgeInsetsDirectional.all(20), + width: 320, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + _buildMonthsGrid(), + const SizedBox(height: 20), + Row( + spacing: 12, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + FilledButton( + onPressed: () => Navigator.pop(context), + style: FilledButton.styleFrom( + fixedSize: const Size(106, 40), + backgroundColor: const Color(0xFFEDF2F7), + padding: const EdgeInsetsDirectional.symmetric( + vertical: 10, + horizontal: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Text( + 'Cancel', + style: context.textTheme.titleSmall?.copyWith( + fontSize: 14, + fontWeight: FontWeight.w600, + color: ColorsManager.grey700, + ), + ), + ), + FilledButton( + onPressed: () { + Navigator.pop(context); + final date = DateTime(_currentYear); + widget.onDateSelected?.call(date); + }, + style: FilledButton.styleFrom( + fixedSize: const Size(106, 40), + backgroundColor: ColorsManager.vividBlue.withValues( + alpha: 0.7, + ), + padding: const EdgeInsetsDirectional.symmetric( + vertical: 10, + horizontal: 16, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Text( + 'Done', + style: context.textTheme.titleSmall?.copyWith( + fontSize: 14, + fontWeight: FontWeight.w600, + color: ColorsManager.whiteColors, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildMonthsGrid() { + return GridView.builder( + shrinkWrap: true, + itemCount: years.length, + physics: const NeverScrollableScrollPhysics(), + gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 3, + childAspectRatio: 2.5, + mainAxisSpacing: 8, + mainAxisExtent: 30, + ), + itemBuilder: (context, index) { + final isSelected = _currentYear == years[index]; + return InkWell( + onTap: () => setState(() => _currentYear = years[index]), + child: DecoratedBox( + decoration: BoxDecoration( + color: const Color(0xFFEDF2F7), + borderRadius: BorderRadius.only( + topLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero, + bottomLeft: index % 3 == 0 ? const Radius.circular(16) : Radius.zero, + topRight: index % 3 == 2 ? const Radius.circular(16) : Radius.zero, + bottomRight: + index % 3 == 2 ? const Radius.circular(16) : Radius.zero, + ), + ), + child: Container( + alignment: Alignment.center, + decoration: BoxDecoration( + color: isSelected + ? ColorsManager.vividBlue.withValues(alpha: 0.7) + : const Color(0xFFEDF2F7), + borderRadius: + isSelected ? BorderRadius.circular(15) : BorderRadius.zero, + ), + child: Text( + years[index].toString(), + style: context.textTheme.titleSmall?.copyWith( + fontSize: 12, + color: isSelected + ? ColorsManager.whiteColors + : ColorsManager.blackColor.withValues(alpha: 0.8), + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart index 012f435a..1acf7df5 100644 --- a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart +++ b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; part 'energy_consumption_by_phases_event.dart'; part 'energy_consumption_by_phases_state.dart'; @@ -31,6 +32,13 @@ class EnergyConsumptionByPhasesBloc chartData: chartData, ), ); + } on APIException catch (e) { + emit( + state.copyWith( + status: EnergyConsumptionByPhasesStatus.failure, + errorMessage: e.message, + ), + ); } catch (e) { emit( state.copyWith( diff --git a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart index c1c51a16..97d182c5 100644 --- a/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart +++ b/lib/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; part 'energy_consumption_per_device_event.dart'; part 'energy_consumption_per_device_state.dart'; @@ -13,7 +14,8 @@ class EnergyConsumptionPerDeviceBloc this._energyConsumptionPerDeviceService, ) : super(const EnergyConsumptionPerDeviceState()) { on(_onLoadEnergyConsumptionPerDeviceEvent); - on(_onClearEnergyConsumptionPerDeviceEvent); + on( + _onClearEnergyConsumptionPerDeviceEvent); } final EnergyConsumptionPerDeviceService _energyConsumptionPerDeviceService; @@ -31,6 +33,13 @@ class EnergyConsumptionPerDeviceBloc chartData: chartData, ), ); + } on APIException catch (e) { + emit( + state.copyWith( + status: EnergyConsumptionPerDeviceStatus.failure, + errorMessage: e.message, + ), + ); } catch (e) { emit( state.copyWith( diff --git a/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart b/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart index d0e7aab6..2aefd798 100644 --- a/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart +++ b/lib/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; part 'power_clamp_info_event.dart'; part 'power_clamp_info_state.dart'; @@ -31,6 +32,13 @@ class PowerClampInfoBloc extends Bloc powerClampModel: powerClampModel, ), ); + } on APIException catch (e) { + emit( + state.copyWith( + status: PowerClampInfoStatus.error, + errorMessage: e.message, + ), + ); } catch (e) { emit( state.copyWith( diff --git a/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart b/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart index 42ad57e8..f51d20cf 100644 --- a/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart +++ b/lib/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart @@ -3,6 +3,7 @@ import 'package:equatable/equatable.dart'; import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart'; import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; part 'total_energy_consumption_event.dart'; part 'total_energy_consumption_state.dart'; @@ -31,6 +32,13 @@ class TotalEnergyConsumptionBloc status: TotalEnergyConsumptionStatus.loaded, ), ); + } on APIException catch (e) { + emit( + state.copyWith( + errorMessage: e.message, + status: TotalEnergyConsumptionStatus.failure, + ), + ); } catch (e) { emit( state.copyWith( diff --git a/lib/pages/analytics/modules/energy_management/helpers/energy_consumption_by_phases_chart_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/energy_consumption_by_phases_chart_helper.dart deleted file mode 100644 index e79fe48d..00000000 --- a/lib/pages/analytics/modules/energy_management/helpers/energy_consumption_by_phases_chart_helper.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart'; - -abstract final class EnergyConsumptionByPhasesChartHelper { - const EnergyConsumptionByPhasesChartHelper._(); - - static const fakeData = [ - PhasesEnergyConsumption(month: 1, phaseA: 200, phaseB: 300, phaseC: 400), - PhasesEnergyConsumption(month: 2, phaseA: 300, phaseB: 400, phaseC: 500), - PhasesEnergyConsumption(month: 3, phaseA: 400, phaseB: 500, phaseC: 600), - PhasesEnergyConsumption(month: 4, phaseA: 100, phaseB: 100, phaseC: 100), - PhasesEnergyConsumption(month: 5, phaseA: 300, phaseB: 400, phaseC: 500), - PhasesEnergyConsumption(month: 6, phaseA: 300, phaseB: 100, phaseC: 400), - PhasesEnergyConsumption(month: 7, phaseA: 300, phaseB: 100, phaseC: 400), - PhasesEnergyConsumption(month: 8, phaseA: 500, phaseB: 100, phaseC: 100), - PhasesEnergyConsumption(month: 9, phaseA: 500, phaseB: 100, phaseC: 200), - PhasesEnergyConsumption(month: 10, phaseA: 100, phaseB: 50, phaseC: 50), - PhasesEnergyConsumption(month: 11, phaseA: 600, phaseB: 750, phaseC: 130), - PhasesEnergyConsumption(month: 12, phaseA: 100, phaseB: 80, phaseC: 100), - ]; -} diff --git a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart index 7e948d21..b1af85c8 100644 --- a/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart +++ b/lib/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart @@ -21,12 +21,13 @@ abstract final class EnergyManagementChartsHelper { reservedSize: 32, showTitles: true, maxIncluded: true, + minIncluded: true, getTitlesWidget: (value, meta) => Padding( padding: const EdgeInsetsDirectional.only(top: 20.0), child: Text( value.toString(), style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.greyColor, + color: ColorsManager.lightGreyColor, fontSize: 12, ), ), @@ -36,7 +37,8 @@ abstract final class EnergyManagementChartsHelper { leftTitles: AxisTitles( sideTitles: SideTitles( showTitles: true, - maxIncluded: true, + maxIncluded: false, + minIncluded: false, interval: leftTitlesInterval, reservedSize: 110, getTitlesWidget: (value, meta) => Padding( @@ -48,7 +50,7 @@ abstract final class EnergyManagementChartsHelper { value.formatNumberToKwh, style: context.textTheme.bodySmall?.copyWith( fontSize: 12, - color: ColorsManager.greyColor, + color: ColorsManager.lightGreyColor, ), ), ), @@ -91,31 +93,40 @@ abstract final class EnergyManagementChartsHelper { ); } - static FlBorderData borderData() { - return FlBorderData( - show: true, - border: const Border.symmetric( - horizontal: BorderSide( - color: ColorsManager.greyColor, - style: BorderStyle.solid, - width: 1, - ), - ), - ); - } - - static FlGridData gridData() { - return const FlGridData( + static FlGridData gridData({ + double horizontalInterval = 250, + }) { + return FlGridData( show: true, drawVerticalLine: false, drawHorizontalLine: true, + horizontalInterval: horizontalInterval, + getDrawingHorizontalLine: (value) { + return FlLine( + color: ColorsManager.greyColor, + strokeWidth: 1, + dashArray: value == 0 ? null : [5, 5], + ); + }, + ); + } + + static FlBorderData borderData() { + return FlBorderData( + border: const Border( + bottom: BorderSide( + color: ColorsManager.greyColor, + style: BorderStyle.solid, + ), + ), + show: true, ); } static LineTouchData lineTouchData() { return LineTouchData( handleBuiltInTouches: true, - touchSpotThreshold: 2, + touchSpotThreshold: 16, touchTooltipData: EnergyManagementChartsHelper.lineTouchTooltipData(), ); } diff --git a/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart b/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart index 4f329104..8de92098 100644 --- a/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart +++ b/lib/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart @@ -1,52 +1,77 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_by_phases/energy_consumption_by_phases_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/total_energy_consumption/total_energy_consumption_bloc.dart'; +import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart'; import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart'; -import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; abstract final class FetchEnergyManagementDataHelper { const FetchEnergyManagementDataHelper._(); - static void fetchEnergyManagementData( - BuildContext context, { - DateTime? selectedDate, - }) { - final (selectedCommunities, selectedSpaces) = - getSelectedCommunitiesAndSpaces(context); + static AnalyticsDevice? getSelectedDevice(BuildContext context) { + return context.read().state.selectedDevice; + } - if (selectedCommunities.isEmpty && selectedSpaces.isEmpty) { + static void loadEnergyManagementData( + BuildContext context, { + required String communityId, + required String spaceId, + DateTime? selectedDate, + bool shouldFetchAnalyticsDevices = true, + }) { + if (communityId.isEmpty && spaceId.isEmpty) { clearAllData(context); return; } - loadTotalEnergyConsumption(context); - loadEnergyConsumptionByPhases(context); - loadEnergyConsumptionPerDevice(context); - return; - } - - static (List selectedCommunities, List selectedSpaces) - getSelectedCommunitiesAndSpaces(BuildContext context) { - final spaceTreeState = context.read().state; - final selectedCommunities = spaceTreeState.selectedCommunities; - final selectedSpaces = spaceTreeState.selectedSpaces; - - return (selectedCommunities, selectedSpaces); + final datePickerState = context.read().state; + final selectedDate0 = selectedDate ?? datePickerState.monthlyDate; + if (shouldFetchAnalyticsDevices) { + loadAnalyticsDevices( + context, + communityUuid: communityId, + spaceUuid: spaceId, + selectedDate: selectedDate0, + ); + loadRealtimeDeviceChanges(context); + loadPowerClampInfo(context); + } + loadTotalEnergyConsumption( + context, + selectedDate: selectedDate0, + spaceId: spaceId, + ); + final selectedDevice = getSelectedDevice(context); + if (selectedDevice case final AnalyticsDevice device) { + loadEnergyConsumptionByPhases( + context, + powerClampUuid: device.uuid, + selectedDate: selectedDate0, + ); + } + loadEnergyConsumptionPerDevice( + context, + spaceId: spaceId, + selectedDate: selectedDate0, + ); } static void loadEnergyConsumptionByPhases( BuildContext context, { + required String powerClampUuid, DateTime? selectedDate, }) { final param = GetEnergyConsumptionByPhasesParam( - startDate: selectedDate, - spaceId: '', + date: selectedDate, + powerClampUuid: powerClampUuid, ); context.read().add( LoadEnergyConsumptionByPhasesEvent(param: param), @@ -56,46 +81,90 @@ abstract final class FetchEnergyManagementDataHelper { static void loadTotalEnergyConsumption( BuildContext context, { DateTime? selectedDate, + required String spaceId, }) { - final (selectedCommunities, selectedSpaces) = - getSelectedCommunitiesAndSpaces(context); - final param = GetTotalEnergyConsumptionParam( - spaceId: selectedCommunities.firstOrNull, - startDate: selectedDate, + spaceId: spaceId, + monthDate: selectedDate, ); context.read().add( TotalEnergyConsumptionLoadEvent(param: param), ); } - static void loadEnergyConsumptionPerDevice(BuildContext context) { - const param = GetEnergyConsumptionPerDeviceParam(); + static void loadEnergyConsumptionPerDevice( + BuildContext context, { + DateTime? selectedDate, + required String spaceId, + }) { + final param = GetEnergyConsumptionPerDeviceParam( + spaceId: spaceId, + monthDate: selectedDate, + ); context.read().add( - const LoadEnergyConsumptionPerDeviceEvent(param), + LoadEnergyConsumptionPerDeviceEvent(param), ); } static void loadPowerClampInfo(BuildContext context) { - context.read().add( - const LoadPowerClampInfoEvent('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'), + final selectedDevice = getSelectedDevice(context); + if (selectedDevice case final AnalyticsDevice device) { + context.read().add( + LoadPowerClampInfoEvent(device.uuid), + ); + } + } + + static void loadRealtimeDeviceChanges( + BuildContext context, { + String? deviceUuid, + }) { + final selectedDevice = getSelectedDevice(context); + + context.read().add( + RealtimeDeviceChangesStarted(deviceUuid ?? selectedDevice?.uuid ?? ''), ); } - static void loadRealtimeDeviceChanges(BuildContext context) { - context.read().add( - const RealtimeDeviceChangesStarted('cb71d6ad-6e29-4eaa-ae3e-1a0d1c5f60fa'), + static void loadAnalyticsDevices( + BuildContext context, { + required String communityUuid, + required String spaceUuid, + required DateTime selectedDate, + }) { + context.read().add( + LoadAnalyticsDevicesEvent( + onSuccess: (device) { + context.read().add( + LoadPowerClampInfoEvent(device.uuid), + ); + loadEnergyConsumptionByPhases( + context, + powerClampUuid: device.uuid, + selectedDate: selectedDate, + ); + context.read().add( + RealtimeDeviceChangesStarted(device.uuid), + ); + }, + param: GetAnalyticsDevicesParam( + communityUuid: communityUuid, + spaceUuid: spaceUuid, + deviceTypes: ['PC'], + requestType: AnalyticsDeviceRequestType.energyManagement, + ), + ), ); } static void clearAllData(BuildContext context) { + context.read().add( + const ClearPowerClampInfoEvent(), + ); context.read().add( const RealtimeDeviceChangesClosed(), ); - context.read().add( - const ClearPowerClampInfoEvent(), - ); context.read().add( const ClearEnergyConsumptionPerDeviceEvent(), ); @@ -107,5 +176,6 @@ abstract final class FetchEnergyManagementDataHelper { context.read().add( const ClearEnergyConsumptionByPhasesEvent(), ); + context.read().add(const ClearAnalyticsDeviceEvent()); } } diff --git a/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart b/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart index cba5eea5..f88febcc 100644 --- a/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart +++ b/lib/pages/analytics/modules/energy_management/views/analytics_energy_management_view.dart @@ -7,7 +7,6 @@ class AnalyticsEnergyManagementView extends StatelessWidget { const AnalyticsEnergyManagementView({super.key}); static const _padding = EdgeInsetsDirectional.all(32); - @override Widget build(BuildContext context) { return LayoutBuilder( diff --git a/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart new file mode 100644 index 00000000..f7b33309 --- /dev/null +++ b/lib/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart @@ -0,0 +1,114 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AnalyticsDeviceDropdown extends StatelessWidget { + const AnalyticsDeviceDropdown({ + required this.onChanged, + this.showSpaceUuid = false, + super.key, + }); + + final ValueChanged onChanged; + final bool showSpaceUuid; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(16), + border: Border.all( + color: ColorsManager.greyColor, + width: 1, + ), + ), + child: Visibility( + visible: state.devices.isNotEmpty, + replacement: _buildNoDevicesFound(context), + child: _buildDevicesDropdown(context, state), + ), + ); + }, + ); + } + + static const _defaultPadding = EdgeInsetsDirectional.symmetric( + horizontal: 20, + vertical: 2, + ); + + Widget _buildNoDevicesFound(BuildContext context) { + return Padding( + padding: _defaultPadding, + child: Text( + 'no devices found', + style: _getTextStyle(context), + ), + ); + } + + Widget _buildDevicesDropdown(BuildContext context, AnalyticsDevicesState state) { + final spaceUuid = state.selectedDevice?.spaceUuid; + return DropdownButton( + value: state.selectedDevice, + isDense: true, + borderRadius: BorderRadius.circular(16), + dropdownColor: ColorsManager.whiteColors, + underline: const SizedBox.shrink(), + icon: const RotatedBox( + quarterTurns: 1, + child: Icon(Icons.chevron_right, size: 16), + ), + style: _getTextStyle(context), + padding: _defaultPadding, + selectedItemBuilder: (context) { + return state.devices.map((e) => Text(e.name)).toList(); + }, + items: state.devices.map((e) { + return DropdownMenuItem( + value: e, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(e.name), + if (showSpaceUuid) + if (spaceUuid != null) + FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerStart, + child: Text( + spaceUuid, + style: _getTextStyle(context)?.copyWith( + fontSize: 10, + ), + ), + ), + ], + ), + ); + }).toList(), + onChanged: (value) { + if (value case final AnalyticsDevice device) { + context.read().add( + SelectAnalyticsDeviceEvent(device), + ); + onChanged.call(device); + } + }, + ); + } + + TextStyle? _getTextStyle(BuildContext context) { + return context.textTheme.labelSmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w700, + fontSize: 14, + ); + } +} diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart index c94755bb..52c6f591 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart.dart @@ -1,6 +1,6 @@ import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/analytics/helpers/get_month_name_from_int.dart'; +import 'package:intl/intl.dart'; import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -18,7 +18,11 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget { Widget build(BuildContext context) { return BarChart( BarChartData( - gridData: EnergyManagementChartsHelper.gridData(), + + gridData: EnergyManagementChartsHelper.gridData().copyWith( + checkToShowHorizontalLine: (value) => true, + horizontalInterval: 250, + ), borderData: EnergyManagementChartsHelper.borderData(), barTouchData: _barTouchData(context), titlesData: _titlesData(context), @@ -31,25 +35,29 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget { barRods: [ BarChartRodData( color: ColorsManager.vividBlue.withValues(alpha: 0.1), - toY: data.phaseA + data.phaseB + data.phaseC, + toY: data.energyConsumedA + + data.energyConsumedB + + data.energyConsumedC, rodStackItems: [ BarChartRodStackItem( 0, - data.phaseA, + data.energyConsumedA, ColorsManager.vividBlue.withValues(alpha: 0.8), ), BarChartRodStackItem( - data.phaseA, - data.phaseA + data.phaseB, + data.energyConsumedA, + data.energyConsumedA + data.energyConsumedB, ColorsManager.vividBlue.withValues(alpha: 0.4), ), BarChartRodStackItem( - data.phaseA + data.phaseB, - data.phaseA + data.phaseB + data.phaseC, + data.energyConsumedA + data.energyConsumedB, + data.energyConsumedA + + data.energyConsumedB + + data.energyConsumedC, ColorsManager.vividBlue.withValues(alpha: 0.15), ), ], - width: 16, + width: 8, borderRadius: const BorderRadius.only( topLeft: Radius.circular(8), topRight: Radius.circular(8), @@ -59,6 +67,7 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget { ); }).toList(), ), + duration: Duration.zero, ); } @@ -91,18 +100,27 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget { }) { final data = energyData; - final month = data[group.x.toInt()].month.getMonthName; - final phaseA = data[group.x.toInt()].phaseA; - final phaseB = data[group.x.toInt()].phaseB; - final phaseC = data[group.x.toInt()].phaseC; + final date = DateFormat('dd/MM/yyyy').format(data[group.x.toInt()].date); + final phaseA = data[group.x.toInt()].energyConsumedA; + final phaseB = data[group.x.toInt()].energyConsumedB; + final phaseC = data[group.x.toInt()].energyConsumedC; + final total = data[group.x.toInt()].energyConsumedKw; return BarTooltipItem( - '$month\n', + '$date\n', context.textTheme.bodyMedium!.copyWith( color: ColorsManager.blackColor, fontSize: 14, ), + textAlign: TextAlign.start, children: [ + TextSpan( + text: 'Total: $total\n', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontSize: 12, + ), + ), TextSpan( text: 'Phase A: $phaseA\n', style: context.textTheme.bodySmall?.copyWith( @@ -144,23 +162,23 @@ class EnergyConsumptionByPhasesChart extends StatelessWidget { sideTitles: SideTitles( showTitles: true, getTitlesWidget: (value, _) { - final month = energyData[value.toInt()].month.getMonthName; + final month = DateFormat('d').format(energyData[value.toInt()].date); return FittedBox( - alignment: AlignmentDirectional.bottomCenter, + alignment: AlignmentDirectional.center, fit: BoxFit.scaleDown, child: RotatedBox( quarterTurns: 3, child: Text( month, style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.greyColor, + color: ColorsManager.lightGreyColor, fontSize: 11, ), ), ), ); }, - reservedSize: 36, + reservedSize: 18, ), ); diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart index 1766266c..1bd1ed9e 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart @@ -19,10 +19,12 @@ class EnergyConsumptionByPhasesChartBox extends StatelessWidget { decoration: secondarySection, child: Column( mainAxisSize: MainAxisSize.min, - spacing: 20, children: [ AnalyticsErrorWidget(state.errorMessage), - EnergyConsumptionByPhasesTitle(isLoading: state.status == EnergyConsumptionByPhasesStatus.loading,), + EnergyConsumptionByPhasesTitle( + isLoading: state.status == EnergyConsumptionByPhasesStatus.loading, + ), + const SizedBox(height: 20), Expanded( child: EnergyConsumptionByPhasesChart( energyData: state.chartData, diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart index 8a779d63..1e74ad31 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart @@ -12,11 +12,16 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget { Widget build(BuildContext context) { return LineChart( LineChartData( + clipData: const FlClipData.vertical(), titlesData: EnergyManagementChartsHelper.titlesData( context, leftTitlesInterval: 250, ), - gridData: EnergyManagementChartsHelper.gridData(), + + gridData: EnergyManagementChartsHelper.gridData().copyWith( + checkToShowHorizontalLine: (value) => true, + horizontalInterval: 250, + ), borderData: EnergyManagementChartsHelper.borderData(), lineTouchData: EnergyManagementChartsHelper.lineTouchData(), lineBarsData: chartData.map((e) { @@ -33,7 +38,7 @@ class EnergyConsumptionPerDeviceChart extends StatelessWidget { ); }).toList(), ), - duration: Durations.extralong1, + duration: Duration.zero, curve: Curves.easeIn, ); } diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart index b62ebe54..be5faf57 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart_box.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/energy_consumption_per_device/energy_consumption_per_device_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_chart.dart'; @@ -22,7 +23,6 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget { ), padding: const EdgeInsets.all(30), child: Column( - spacing: 20, crossAxisAlignment: CrossAxisAlignment.start, children: [ AnalyticsErrorWidget(state.errorMessage), @@ -46,11 +46,14 @@ class EnergyConsumptionPerDeviceChartBox extends StatelessWidget { flex: 2, child: EnergyConsumptionPerDeviceDevicesList( chartData: state.chartData, + devices: context.watch().state.devices, ), ), ], ), + const SizedBox(height: 20), const Divider(height: 0), + const SizedBox(height: 20), Expanded( child: EnergyConsumptionPerDeviceChart(chartData: state.chartData), ), diff --git a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart index da7a59a8..f0cb5d64 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/energy_consumption_per_device_devices_list.dart @@ -1,10 +1,16 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/chart_informative_cell.dart'; class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget { - const EnergyConsumptionPerDeviceDevicesList({required this.chartData, super.key}); + const EnergyConsumptionPerDeviceDevicesList({ + required this.chartData, + required this.devices, + super.key, + }); + final List devices; final List chartData; @override @@ -16,47 +22,27 @@ class EnergyConsumptionPerDeviceDevicesList extends StatelessWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end, - children: chartData.map((e) => _buildDeviceCell(context, e)).toList(), + children: devices.map((e) => _buildDeviceCell(context, e)).toList(), ), ); } - Widget _buildDeviceCell(BuildContext context, DeviceEnergyDataModel device) { - return Container( - height: MediaQuery.sizeOf(context).height * 0.0365, - padding: const EdgeInsetsDirectional.symmetric( - vertical: 8, - horizontal: 12, - ), - decoration: BoxDecoration( - borderRadius: BorderRadiusDirectional.circular(8), - border: Border.all( - color: ColorsManager.greyColor, - width: 1, - ), - ), - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: Alignment.center, - child: Row( - spacing: 6, - children: [ - CircleAvatar( - radius: 4, - backgroundColor: device.color, - ), - Text( - device.deviceName, - textAlign: TextAlign.center, - style: const TextStyle( - color: ColorsManager.blackColor, - fontWeight: FontWeight.w400, - fontSize: 14, - ), - ), - ], - ), - ), + Widget _buildDeviceCell(BuildContext context, AnalyticsDevice device) { + final deviceColor = chartData + .firstWhere( + (element) => element.deviceId == device.uuid, + orElse: () => const DeviceEnergyDataModel( + energy: [], + deviceName: '', + deviceId: '', + color: Colors.red, + ), + ) + .color; + + return Tooltip( + message: '${device.name}\n${device.spaceUuid ?? ''}', + child: ChartInformativeCell(title: Text(device.name), color: deviceColor), ); } } diff --git a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.dart b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.dart deleted file mode 100644 index 20540328..00000000 --- a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; - -class PowerClampEnergyDataDeviceDropdown extends StatelessWidget { - const PowerClampEnergyDataDeviceDropdown({super.key}); - - static final _color = ColorsManager.blackColor.withValues(alpha: 0.8); - - @override - Widget build(BuildContext context) { - return TextButton( - style: TextButton.styleFrom( - foregroundColor: _color, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: const BorderSide( - color: ColorsManager.greyColor, - width: 1, - ), - ), - backgroundColor: ColorsManager.transparentColor, - padding: const EdgeInsets.symmetric( - horizontal: 32, - vertical: 16, - ), - ), - child: const Text( - 'Device 1', - style: TextStyle( - fontWeight: FontWeight.w700, - ), - ), - onPressed: () {}, - ); - } -} diff --git a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart index 6bb56071..f95ff7d1 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_widget.dart @@ -1,17 +1,17 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/power_clamp_info/power_clamp_info_bloc.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/energy_consumption_by_phases_chart_box.dart'; -import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_data_device_dropdown.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart'; import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_sidebar_header.dart'; import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; -import 'package:syncrow_web/utils/extension/build_context_x.dart'; import 'package:syncrow_web/utils/style.dart'; class PowerClampEnergyDataWidget extends StatelessWidget { @@ -39,25 +39,18 @@ class PowerClampEnergyDataWidget extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ AnalyticsErrorWidget(state.errorMessage), - _buildHeader(context), - Text( - 'Device ID:', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.textPrimaryColor, - fontWeight: FontWeight.w400, - fontSize: 12, - ), + AnalyticsSidebarHeader( + title: 'Smart Power Clamp', + showSpaceUuidInDevicesDropdown: true, + onChanged: (device) { + FetchEnergyManagementDataHelper.loadEnergyConsumptionByPhases( + context, + powerClampUuid: device.uuid, + selectedDate: + context.read().state.monthlyDate, + ); + }, ), - const SizedBox(height: 6), - SelectableText( - state.powerClampModel?.productUuid ?? 'N/A', - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.blackColor, - fontWeight: FontWeight.w400, - fontSize: 12, - ), - ), - const Divider(), Expanded( flex: 2, child: PowerClampEnergyStatusWidget( @@ -65,13 +58,18 @@ class PowerClampEnergyDataWidget extends StatelessWidget { PowerClampEnergyStatus( iconPath: Assets.powerActiveIcon, title: 'Active', - value: _valueFromCode('EnergyConsumed', generalDataPoints), + value: _valueFromCode('ActivePower', generalDataPoints), unit: 'W', ), PowerClampEnergyStatus( iconPath: Assets.voltMeterIcon, title: 'Current', - value: _valueFromCode('Current', generalDataPoints), + value: _valueFromCode('Current', generalDataPoints) + .replaceAllMapped( + RegExp(r'(\d{1,3})(?=(\d{3})+(?!\d))'), + (Match m) => '${m[1]}.', + ) + .replaceAll('.0', ''), unit: 'A', ), PowerClampEnergyStatus( @@ -102,37 +100,6 @@ class PowerClampEnergyDataWidget extends StatelessWidget { ); } - Widget _buildHeader(BuildContext context) { - return Row( - mainAxisSize: MainAxisSize.min, - children: [ - Expanded( - flex: 2, - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.centerStart, - child: SelectableText( - 'Smart Power Clamp', - style: context.textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w700, - color: ColorsManager.vividBlue.withValues(alpha: 0.6), - fontSize: 18, - ), - ), - ), - ), - const Spacer(), - const Expanded( - child: FittedBox( - fit: BoxFit.scaleDown, - alignment: AlignmentDirectional.centerEnd, - child: PowerClampEnergyDataDeviceDropdown(), - ), - ), - ], - ); - } - String _valueFromCode(String code, List points) { return points .firstWhere((e) => e.code == code, orElse: () => DataPoint(value: '--')) diff --git a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart index 27d74ae0..fc957035 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart @@ -48,6 +48,9 @@ class PowerClampEnergyStatusWidget extends StatelessWidget { fontWeight: FontWeight.w400, fontSize: 16, ), + softWrap: true, + maxLines: 2, + overflow: TextOverflow.ellipsis, ), trailing: Text.rich( TextSpan( diff --git a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart index 1cb20aac..a96a7298 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/power_clamp_phases_data_widget.dart @@ -55,7 +55,7 @@ class PowerClampPhasesDataWidget extends StatelessWidget { iconPath: Assets.powerActiveIcon, title: 'Active Power', value: _valueFromCode( - code: 'ReactivePower$phaseSuffix', + code: 'ActivePower$phaseSuffix', points: phase?.dataPoints, ), unit: 'W', @@ -125,7 +125,48 @@ class PowerClampPhasesDataWidget extends StatelessWidget { (e) => e.code == code, orElse: () => DataPoint(value: '--'), ); + final value = element?.value; + if (code.contains('Current')) { + return _formatCurrentValue(value?.toString()); + } + if (code.contains('PowerFactor')) { + return _formatPowerFactor(value?.toString()); + } + if (code.contains('Voltage')) { + return _formatVoltage(value?.toString()); + } + return value?.toString() ?? '--'; + } - return element?.value.toString() ?? '--'; + String _formatCurrentValue(String? value) { + if (value == null) return '--'; + String str = value; + if (str.isEmpty || str == '--') return '--'; + str = str.replaceAll(RegExp(r'[^0-9]'), ''); + if (str.isEmpty) return '--'; + if (str.length == 1) return '${str[0]}.0'; + return '${str[0]}.${str.substring(1)}'; + } + + String _formatPowerFactor(String? value) { + if (value == null) return '--'; + String str = value; + if (str.isEmpty || str == '--') return '--'; + str = str.replaceAll(RegExp(r'[^0-9]'), ''); + if (str.isEmpty) return '--'; + final intValue = int.tryParse(str); + if (intValue == null) return '--'; + final doubleValue = intValue / 100; + return doubleValue.toStringAsFixed(2); + } + + String _formatVoltage(String? value) { + if (value == null) return '--'; + String str = value; + if (str.isEmpty || str == '--') return '--'; + str = str.replaceAll(RegExp(r'[^0-9]'), ''); + if (str.isEmpty) return '--'; + if (str.length == 1) return '0.${str[0]}'; + return '${str.substring(0, str.length - 1)}.${str.substring(str.length - 1)}'; } } diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart index ddf016be..85b95c29 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart.dart @@ -4,15 +4,6 @@ import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; import 'package:syncrow_web/utils/color_manager.dart'; -// energy_consumption_chart will return id, name and consumption -const phasesJson = { - "1": { - "phaseOne": 1000, - "phaseTwo": 2000, - "phaseThree": 3000, - } -}; - class TotalEnergyConsumptionChart extends StatelessWidget { const TotalEnergyConsumptionChart({required this.chartData, super.key}); @@ -23,17 +14,22 @@ class TotalEnergyConsumptionChart extends StatelessWidget { return Expanded( child: LineChart( LineChartData( + clipData: const FlClipData.vertical(), titlesData: EnergyManagementChartsHelper.titlesData( context, - leftTitlesInterval: 5000, + leftTitlesInterval: 250, + ), + gridData: EnergyManagementChartsHelper.gridData().copyWith( + checkToShowHorizontalLine: (value) => true, + horizontalInterval: 250, ), - gridData: EnergyManagementChartsHelper.gridData(), borderData: EnergyManagementChartsHelper.borderData(), lineTouchData: EnergyManagementChartsHelper.lineTouchData(), lineBarsData: _lineBarsData, ), - duration: Durations.extralong1, + duration: Duration.zero, curve: Curves.easeIn, + ), ); } @@ -41,15 +37,12 @@ class TotalEnergyConsumptionChart extends StatelessWidget { List get _lineBarsData { return [ LineChartBarData( - preventCurveOvershootingThreshold: 0.1, - curveSmoothness: 0.55, - preventCurveOverShooting: true, spots: chartData .asMap() .entries .map( (entry) => FlSpot( - entry.key.toDouble(), + entry.value.date.day.toDouble(), entry.value.value, ), ) diff --git a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart index 9e70e45e..e197c297 100644 --- a/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart +++ b/lib/pages/analytics/modules/energy_management/widgets/total_energy_consumption_chart_box.dart @@ -19,7 +19,6 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget { ), padding: const EdgeInsets.all(30), child: Column( - spacing: 20, crossAxisAlignment: CrossAxisAlignment.start, children: [ AnalyticsErrorWidget(state.errorMessage), @@ -39,7 +38,9 @@ class TotalEnergyConsumptionChartBox extends StatelessWidget { const Spacer(flex: 4), ], ), + const SizedBox(height: 20), const Divider(), + const SizedBox(height: 20), TotalEnergyConsumptionChart(chartData: state.chartData), ], ), diff --git a/lib/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart b/lib/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart new file mode 100644 index 00000000..110f3c60 --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart @@ -0,0 +1,40 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/analytics/models/occupacy.dart'; +import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart'; +import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; + +part 'occupancy_event.dart'; +part 'occupancy_state.dart'; + +class OccupancyBloc extends Bloc { + OccupancyBloc(this._occupacyService) : super(const OccupancyState()) { + on(_onLoadOccupancyEvent); + on(_onClearOccupancyEvent); + } + + final OccupacyService _occupacyService; + + Future _onLoadOccupancyEvent( + LoadOccupancyEvent event, + Emitter emit, + ) async { + emit(state.copyWith(status: OccupancyStatus.loading)); + try { + final chartData = await _occupacyService.load(event.param); + emit(state.copyWith(chartData: chartData, status: OccupancyStatus.loaded)); + } on APIException catch (e) { + emit(state.copyWith(status: OccupancyStatus.failure, errorMessage: e.message)); + } catch (e) { + emit(state.copyWith(status: OccupancyStatus.failure, errorMessage: '$e')); + } + } + + void _onClearOccupancyEvent( + ClearOccupancyEvent event, + Emitter emit, + ) { + emit(const OccupancyState()); + } +} diff --git a/lib/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_event.dart b/lib/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_event.dart new file mode 100644 index 00000000..8e1ef1c1 --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_event.dart @@ -0,0 +1,21 @@ +part of 'occupancy_bloc.dart'; + +sealed class OccupancyEvent extends Equatable { + const OccupancyEvent(); + + @override + List get props => []; +} + +final class LoadOccupancyEvent extends OccupancyEvent { + const LoadOccupancyEvent(this.param); + + final GetOccupancyParam param; + + @override + List get props => [param]; +} + +final class ClearOccupancyEvent extends OccupancyEvent { + const ClearOccupancyEvent(); +} diff --git a/lib/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_state.dart b/lib/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_state.dart new file mode 100644 index 00000000..88997847 --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_state.dart @@ -0,0 +1,30 @@ +part of 'occupancy_bloc.dart'; + +enum OccupancyStatus { initial, loading, loaded, failure } + +final class OccupancyState extends Equatable { + const OccupancyState({ + this.chartData = const [], + this.status = OccupancyStatus.initial, + this.errorMessage, + }); + + final List chartData; + final OccupancyStatus status; + final String? errorMessage; + + OccupancyState copyWith({ + List? chartData, + OccupancyStatus? status, + String? errorMessage, + }) { + return OccupancyState( + chartData: chartData ?? this.chartData, + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [chartData, status, errorMessage]; +} diff --git a/lib/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart b/lib/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart new file mode 100644 index 00000000..453b68ce --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart @@ -0,0 +1,57 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart'; +import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; + +part 'occupancy_heat_map_event.dart'; +part 'occupancy_heat_map_state.dart'; + +class OccupancyHeatMapBloc + extends Bloc { + OccupancyHeatMapBloc( + this._occupancyHeatMapService, + ) : super(const OccupancyHeatMapState()) { + on(_onLoadOccupancyHeatMapEvent); + on(_onClearOccupancyHeatMapEvent); + } + final OccupancyHeatMapService _occupancyHeatMapService; + + Future _onLoadOccupancyHeatMapEvent( + LoadOccupancyHeatMapEvent event, + Emitter emit, + ) async { + emit(state.copyWith(status: OccupancyHeatMapStatus.loading)); + try { + final occupancyHeatMap = await _occupancyHeatMapService.load(event.param); + emit( + state.copyWith( + status: OccupancyHeatMapStatus.loaded, + heatMapData: occupancyHeatMap, + ), + ); + } on APIException catch (e) { + emit( + state.copyWith( + status: OccupancyHeatMapStatus.failure, + errorMessage: e.message, + ), + ); + } catch (e) { + emit( + state.copyWith( + status: OccupancyHeatMapStatus.failure, + errorMessage: e.toString(), + ), + ); + } + } + + void _onClearOccupancyHeatMapEvent( + ClearOccupancyHeatMapEvent event, + Emitter emit, + ) { + emit(const OccupancyHeatMapState()); + } +} diff --git a/lib/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_event.dart b/lib/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_event.dart new file mode 100644 index 00000000..a1e0559a --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_event.dart @@ -0,0 +1,21 @@ +part of 'occupancy_heat_map_bloc.dart'; + +sealed class OccupancyHeatMapEvent extends Equatable { + const OccupancyHeatMapEvent(); + + @override + List get props => []; +} + +final class LoadOccupancyHeatMapEvent extends OccupancyHeatMapEvent { + const LoadOccupancyHeatMapEvent(this.param); + + final GetOccupancyHeatMapParam param; + + @override + List get props => [param]; +} + +final class ClearOccupancyHeatMapEvent extends OccupancyHeatMapEvent { + const ClearOccupancyHeatMapEvent(); +} diff --git a/lib/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_state.dart b/lib/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_state.dart new file mode 100644 index 00000000..b477fed1 --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_state.dart @@ -0,0 +1,30 @@ +part of 'occupancy_heat_map_bloc.dart'; + +enum OccupancyHeatMapStatus { initial, loading, loaded, failure } + +final class OccupancyHeatMapState extends Equatable { + const OccupancyHeatMapState({ + this.status = OccupancyHeatMapStatus.initial, + this.heatMapData = const [], + this.errorMessage, + }); + + final OccupancyHeatMapStatus status; + final String? errorMessage; + final List heatMapData; + + OccupancyHeatMapState copyWith({ + OccupancyHeatMapStatus? status, + List? heatMapData, + String? errorMessage, + }) { + return OccupancyHeatMapState( + status: status ?? this.status, + heatMapData: heatMapData ?? this.heatMapData, + errorMessage: errorMessage ?? this.errorMessage, + ); + } + + @override + List get props => [status, errorMessage, heatMapData]; +} diff --git a/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart b/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart new file mode 100644 index 00000000..0b01fda2 --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart'; +import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; +import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart'; +import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart'; + +abstract final class FetchOccupancyDataHelper { + const FetchOccupancyDataHelper._(); + + static void loadOccupancyData( + BuildContext context, { + required String communityId, + required String spaceId, + }) { + if (communityId.isEmpty && spaceId.isEmpty) { + clearAllData(context); + return; + } + + final datePickerState = context.read().state; + + loadAnalyticsDevices(context, communityUuid: communityId, spaceUuid: spaceId); + final selectedDevice = context.read().state.selectedDevice; + + loadOccupancyChartData( + context, + spaceUuid: spaceId, + date: datePickerState.monthlyDate, + ); + loadHeatMapData(context, spaceUuid: spaceId, year: datePickerState.yearlyDate); + + if (selectedDevice case final AnalyticsDevice device) { + context.read() + ..add(const RealtimeDeviceChangesClosed()) + ..add( + RealtimeDeviceChangesStarted(device.uuid), + ); + } + } + + static void loadHeatMapData( + BuildContext context, { + required String spaceUuid, + required DateTime year, + }) { + context.read().add( + LoadOccupancyHeatMapEvent( + GetOccupancyHeatMapParam(spaceUuid: spaceUuid, year: year), + ), + ); + } + + static void loadOccupancyChartData( + BuildContext context, { + required String spaceUuid, + required DateTime date, + }) { + context.read().add( + LoadOccupancyEvent( + GetOccupancyParam( + monthDate: '${date.year}-${date.month.toString().padLeft(2, '0')}', + spaceUuid: spaceUuid, + ), + ), + ); + } + + static void loadAnalyticsDevices( + BuildContext context, { + required String communityUuid, + required String spaceUuid, + }) { + context.read().add( + LoadAnalyticsDevicesEvent( + param: GetAnalyticsDevicesParam( + communityUuid: communityUuid, + spaceUuid: spaceUuid, + deviceTypes: ['WPS', 'CPS'], + requestType: AnalyticsDeviceRequestType.occupancy, + ), + onSuccess: (device) { + context.read() + ..add(const RealtimeDeviceChangesClosed()) + ..add(RealtimeDeviceChangesStarted(device.uuid)); + }, + ), + ); + } + + static void clearAllData(BuildContext context) { + context.read().add( + const ClearOccupancyEvent(), + ); + context.read().add( + const ClearOccupancyHeatMapEvent(), + ); + context.read().add( + const RealtimeDeviceChangesClosed(), + ); + + context.read().add( + const ClearAnalyticsDeviceEvent(), + ); + } +} diff --git a/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart index 8042ff8b..679c9927 100644 --- a/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart +++ b/lib/pages/analytics/modules/occupancy/views/analytics_occupancy_view.dart @@ -1,12 +1,56 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart'; class AnalyticsOccupancyView extends StatelessWidget { const AnalyticsOccupancyView({super.key}); + static const _padding = EdgeInsetsDirectional.all(32); + @override Widget build(BuildContext context) { - return const Center( - child: Text('AnalyticsOccupancyView is Working!'), + final height = MediaQuery.sizeOf(context).height; + return LayoutBuilder( + builder: (context, constraints) { + final isMediumOrLess = constraints.maxWidth <= 900; + if (isMediumOrLess) { + return SingleChildScrollView( + padding: _padding, + child: Column( + spacing: 32, + children: [ + SizedBox(height: height * 0.46, child: const OccupancyEndSideBar()), + SizedBox(height: height * 0.5, child: const OccupancyChartBox()), + SizedBox(height: height * 0.5, child: const OccupancyHeatMapBox()), + ], + ), + ); + } + + return SingleChildScrollView( + child: Container( + padding: _padding, + height: height * 0.9, + child: const Row( + spacing: 32, + children: [ + Expanded( + flex: 5, + child: Column( + spacing: 20, + children: [ + Expanded(child: OccupancyChartBox()), + Expanded(child: OccupancyHeatMapBox()), + ], + ), + ), + Expanded(flex: 2, child: OccupancyEndSideBar()), + ], + ), + ), + ); + }, ); } } diff --git a/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart b/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart new file mode 100644 index 00000000..c7695064 --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart @@ -0,0 +1,54 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class HeatMapTooltip extends StatelessWidget { + const HeatMapTooltip({ + required this.date, + required this.value, + super.key, + }); + + final DateTime date; + final int value; + + @override + Widget build(BuildContext context) { + return FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.topStart, + child: Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: ColorsManager.grey700, + borderRadius: BorderRadius.circular(3), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Text( + DateFormat('MMM d, yyyy').format(date), + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + fontWeight: FontWeight.w700, + color: ColorsManager.whiteColors, + ), + ), + const Divider(height: 2, thickness: 1), + Text( + '$value Occupants', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 10, + fontWeight: FontWeight.w500, + color: ColorsManager.whiteColors, + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart new file mode 100644 index 00000000..a652ae73 --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart @@ -0,0 +1,108 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/heat_map_tooltip.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart'; + +class InteractiveHeatMap extends StatefulWidget { + const InteractiveHeatMap({ + required this.items, + required this.maxValue, + required this.cellSize, + super.key, + }); + + final List items; + final int maxValue; + final double cellSize; + + @override + State createState() => _InteractiveHeatMapState(); +} + +class _InteractiveHeatMapState extends State { + OccupancyPaintItem? _hoveredItem; + OverlayEntry? _overlayEntry; + final LayerLink _layerLink = LayerLink(); + + @override + void dispose() { + _removeOverlay(); + _overlayEntry?.dispose(); + super.dispose(); + } + + void _removeOverlay() { + _overlayEntry?.remove(); + _overlayEntry = null; + } + + void _showTooltip(OccupancyPaintItem item, Offset localPosition) { + _removeOverlay(); + + final column = item.index ~/ 7; + final row = item.index % 7; + final x = column * widget.cellSize; + final y = row * widget.cellSize; + + _overlayEntry = OverlayEntry( + builder: (context) => Positioned( + child: CompositedTransformFollower( + link: _layerLink, + offset: Offset(x + widget.cellSize, y), + child: Material( + color: Colors.transparent, + child: Transform.translate( + offset: Offset(-(widget.cellSize * 2.5), -50), + child: HeatMapTooltip(date: item.date, value: item.value), + ), + ), + ), + ), + ); + + Overlay.of(context).insert(_overlayEntry!); + } + + @override + Widget build(BuildContext context) { + return CompositedTransformTarget( + link: _layerLink, + child: MouseRegion( + onHover: (event) { + final column = event.localPosition.dx ~/ widget.cellSize; + final row = event.localPosition.dy ~/ widget.cellSize; + final index = column * 7 + row; + + if (index >= 0 && index < widget.items.length) { + final item = widget.items[index]; + if (_hoveredItem != item) { + setState(() => _hoveredItem = item); + _showTooltip(item, event.localPosition); + } + } else { + _removeOverlay(); + setState(() => _hoveredItem = null); + } + }, + onExit: (_) { + _removeOverlay(); + setState(() => _hoveredItem = null); + }, + child: CustomPaint( + isComplex: true, + size: _painterSize, + painter: OccupancyPainter( + items: widget.items, + maxValue: widget.maxValue, + hoveredItem: _hoveredItem, + ), + ), + ), + ); + } + + Size get _painterSize { + final height = 7 * widget.cellSize; + final width = widget.items.length ~/ 7 * widget.cellSize; + return Size(width, height); + } +} diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart new file mode 100644 index 00000000..70087c46 --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart @@ -0,0 +1,153 @@ +import 'package:fl_chart/fl_chart.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/models/occupacy.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/energy_management_charts_helper.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class OccupancyChart extends StatelessWidget { + const OccupancyChart({required this.chartData, super.key}); + + final List chartData; + + static const _chartWidth = 16.0; + + @override + Widget build(BuildContext context) { + return BarChart( + BarChartData( + maxY: 100.001, + gridData: EnergyManagementChartsHelper.gridData().copyWith( + checkToShowHorizontalLine: (value) => true, + horizontalInterval: 20, + ), + borderData: EnergyManagementChartsHelper.borderData(), + barTouchData: _barTouchData(context), + titlesData: _titlesData(context).copyWith( + leftTitles: _titlesData(context).leftTitles.copyWith( + sideTitles: _titlesData(context).leftTitles.sideTitles.copyWith( + maxIncluded: true, + minIncluded: true, + ), + ), + ), + barGroups: List.generate(chartData.length, (index) { + final actual = chartData[index]; + final occupancyValue = double.parse(actual.occupancy); + return BarChartGroupData( + x: index, + barsSpace: 0, + groupVertically: true, + barRods: [ + BarChartRodData( + toY: 100.0, + fromY: occupancyValue == 0 ? occupancyValue : occupancyValue + 2.5, + color: ColorsManager.graysColor, + width: _chartWidth, + borderRadius: BorderRadius.circular(10), + ), + BarChartRodData( + toY: occupancyValue, + color: ColorsManager.vividBlue.withValues(alpha: 0.8), + width: _chartWidth, + borderRadius: BorderRadius.circular(10), + ), + ], + ); + }), + ), + ); + } + + BarTouchData _barTouchData(BuildContext context) { + return BarTouchData( + touchTooltipData: BarTouchTooltipData( + getTooltipColor: (touchTooltipItem) => ColorsManager.whiteColors, + tooltipBorder: const BorderSide( + color: ColorsManager.semiTransparentBlack, + ), + tooltipRoundedRadius: 16, + tooltipPadding: const EdgeInsets.all(8), + getTooltipItem: (group, groupIndex, rod, rodIndex) => getTooltipItem( + context: context, + group: group, + groupIndex: groupIndex, + rod: rod, + rodIndex: rodIndex, + ), + ), + ); + } + + BarTooltipItem? getTooltipItem({ + required BuildContext context, + required BarChartGroupData group, + required int groupIndex, + required BarChartRodData rod, + required int rodIndex, + }) { + final data = chartData; + + final occupancyValue = double.parse(data[group.x.toInt()].occupancy); + final percentage = '${(occupancyValue).toStringAsFixed(0)}%'; + + return BarTooltipItem( + percentage, + context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.blackColor, + fontSize: 14, + ), + ); + } + + FlTitlesData _titlesData(BuildContext context) { + final titlesData = EnergyManagementChartsHelper.titlesData( + context, + leftTitlesInterval: 250, + ); + + final leftTitles = titlesData.leftTitles.copyWith( + sideTitles: titlesData.leftTitles.sideTitles.copyWith( + reservedSize: 70, + interval: 20, + getTitlesWidget: (value, meta) => Padding( + padding: const EdgeInsetsDirectional.only(end: 12), + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: Text( + '${(value).toStringAsFixed(0)}%', + style: context.textTheme.bodySmall?.copyWith( + fontSize: 12, + color: ColorsManager.greyColor, + ), + ), + ), + ), + ), + ); + + final bottomTitles = AxisTitles( + sideTitles: SideTitles( + showTitles: true, + getTitlesWidget: (value, _) => FittedBox( + alignment: AlignmentDirectional.bottomCenter, + fit: BoxFit.scaleDown, + child: Text( + chartData[value.toInt()].date.day.toString(), + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.greyColor, + fontSize: 8, + ), + ), + ), + reservedSize: 36, + ), + ); + + return titlesData.copyWith( + leftTitles: leftTitles, + bottomTitles: bottomTitles, + ); + } +} diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart new file mode 100644 index 00000000..08f7223f --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_chart_box.dart @@ -0,0 +1,77 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy/occupancy_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_chart.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class OccupancyChartBox extends StatelessWidget { + const OccupancyChartBox({super.key}); + + @override + Widget build(BuildContext context) { + final spaceTreeState = context.watch().state; + return BlocBuilder( + builder: (context, state) { + return Container( + padding: const EdgeInsets.all(30), + decoration: containerWhiteDecoration, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnalyticsErrorWidget(state.errorMessage), + Row( + children: [ + const Expanded( + flex: 3, + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: ChartTitle(title: Text('Occupancy')), + ), + ), + const Spacer(), + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerEnd, + child: AnalyticsDateFilterButton( + onDateSelected: (DateTime value) { + context.read().add( + UpdateAnalyticsDatePickerEvent(montlyDate: value), + ); + if (spaceTreeState.selectedSpaces.isNotEmpty) { + FetchOccupancyDataHelper.loadOccupancyChartData( + context, + spaceUuid: + spaceTreeState.selectedSpaces.firstOrNull ?? '', + date: value, + ); + } + }, + selectedDate: context + .watch() + .state + .monthlyDate, + ), + ), + ), + ], + ), + const SizedBox(height: 20), + const Divider(), + const SizedBox(height: 20), + Expanded(child: OccupancyChart(chartData: state.chartData)), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart new file mode 100644 index 00000000..b3f162fa --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_end_side_bar.dart @@ -0,0 +1,79 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/models/power_clamp_energy_status.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/blocs/realtime_device_changes/realtime_device_changes_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/power_clamp_energy_status_widget.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_sidebar_header.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class OccupancyEndSideBar extends StatelessWidget { + const OccupancyEndSideBar({super.key}); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + return Container( + decoration: subSectionContainerDecoration.copyWith( + borderRadius: BorderRadius.circular(30), + ), + padding: const EdgeInsetsDirectional.all(32), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const AnalyticsSidebarHeader(title: 'Presnce Sensor'), + SizedBox( + height: MediaQuery.sizeOf(context).height * 0.2, + child: PowerClampEnergyStatusWidget( + status: [ + PowerClampEnergyStatus( + iconPath: Assets.presenceState, + title: 'Presence Status', + value: _valueFromCode( + 'presence_state', + state.deviceStatusList, + ), + unit: '', + ), + PowerClampEnergyStatus( + iconPath: Assets.presenceTimeIcon, + title: 'Presence Time', + value: + '${_valueFromCode('none_body_time', state.deviceStatusList)} Min', + unit: '', + ), + PowerClampEnergyStatus( + iconPath: Assets.currentDistanceIcon, + title: 'Detection Distance', + value: + '${_valueFromCode('space_move_val', state.deviceStatusList)} M', + unit: '', + ), + ], + ), + ), + const SizedBox(height: 20), + ], + ), + ); + }, + ); + } + + String _valueFromCode( + String code, + List status, { + String? defaultValue, + }) { + final value = status + .firstWhere( + (e) => e.code == code, + orElse: () => Status(code: '--', value: '--'), + ) + .value + .toString(); + return value == 'null' ? defaultValue ?? '--' : value; + } +} diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart new file mode 100644 index 00000000..05415421 --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart @@ -0,0 +1,83 @@ +import 'dart:math' as math show max; + +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/interactive_heat_map.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_gradient.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_months.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class OccupancyHeatMap extends StatelessWidget { + const OccupancyHeatMap({required this.heatMapData, super.key}); + final Map heatMapData; + + static const _cellSize = 16.0; + static const _totalWeeks = 53; + + int get _maxValue => heatMapData.isNotEmpty + ? heatMapData.keys.map((key) => heatMapData[key] ?? 0).reduce(math.max) + : 0; + + DateTime _getStartingDate() { + final jan1 = DateTime(DateTime.now().year, 1, 1); + final startOfWeek = jan1.subtract(Duration(days: jan1.weekday - 1)); + return startOfWeek; + } + + List _generatePaintItems(DateTime startDate) { + return List.generate(_totalWeeks * 7, (index) { + final date = startDate.add(Duration(days: index)); + final value = heatMapData[date] ?? 0; + return OccupancyPaintItem(index: index, value: value, date: date); + }); + } + + @override + Widget build(BuildContext context) { + final startDate = _getStartingDate(); + final paintItems = _generatePaintItems(startDate); + + return Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OccupancyHeatMapMonths(startDate: startDate, cellSize: _cellSize), + Container( + decoration: const BoxDecoration( + border: Border( + bottom: BorderSide(color: ColorsManager.grayBorder), + top: BorderSide(color: ColorsManager.grayBorder), + ), + ), + width: double.infinity, + child: Row( + children: [ + Expanded( + child: FittedBox( + fit: BoxFit.fill, + child: Row( + children: [ + const OccupancyHeatMapDays(cellSize: _cellSize), + SizedBox( + width: _totalWeeks * _cellSize, + height: 7 * _cellSize, + child: InteractiveHeatMap( + items: paintItems, + maxValue: _maxValue, + cellSize: _cellSize, + ), + ), + ], + ), + ), + ), + ], + ), + ), + const SizedBox(height: 20), + OccupancyHeatMapGradient(maxValue: _maxValue), + ], + ); + } +} diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart new file mode 100644 index 00000000..c3b537e0 --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_box.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_date_picker_bloc/analytics_date_picker_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/widgets/analytics_date_filter_button.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/chart_title.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/blocs/occupancy_heat_map/occupancy_heat_map_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/helpers/fetch_occupancy_data_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map.dart'; +import 'package:syncrow_web/pages/analytics/widgets/analytics_error_widget.dart'; +import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/utils/style.dart'; + +class OccupancyHeatMapBox extends StatelessWidget { + const OccupancyHeatMapBox({super.key}); + + @override + Widget build(BuildContext context) { + final spaceTreeState = context.watch().state; + return BlocBuilder( + builder: (context, state) { + return Container( + padding: const EdgeInsets.all(30), + decoration: containerWhiteDecoration, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + AnalyticsErrorWidget(state.errorMessage), + Row( + children: [ + const Expanded( + flex: 3, + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: ChartTitle(title: Text('Occupancy Heat Map')), + ), + ), + const Spacer(), + Expanded( + child: FittedBox( + fit: BoxFit.scaleDown, + alignment: AlignmentDirectional.centerEnd, + child: AnalyticsDateFilterButton( + onDateSelected: (DateTime value) { + context.read().add( + UpdateAnalyticsDatePickerEvent(yearlyDate: value), + ); + if (spaceTreeState.selectedSpaces.isNotEmpty) { + FetchOccupancyDataHelper.loadHeatMapData( + context, + spaceUuid: + spaceTreeState.selectedSpaces.firstOrNull ?? '', + year: value, + ); + } + }, + datePickerType: DatePickerType.year, + selectedDate: context + .watch() + .state + .yearlyDate, + ), + ), + ), + ], + ), + const SizedBox(height: 20), + const Divider(), + const SizedBox(height: 20), + Expanded( + child: OccupancyHeatMap( + heatMapData: state.heatMapData.asMap().map( + (_, value) => MapEntry( + value.eventDate, + value.countTotalPresenceDetected, + ), + ), + ), + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart new file mode 100644 index 00000000..ff754581 --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class OccupancyHeatMapDays extends StatelessWidget { + const OccupancyHeatMapDays({ + required this.cellSize, + this.textColor = ColorsManager.blackColor, + super.key, + }); + + final double cellSize; + final Color textColor; + + static const _weekDayLabels = [ + 'Monday', + 'Tuesday', + 'Wednesday', + 'Thursday', + 'Friday', + 'Saturday', + 'Sunday', + ]; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: List.generate(7, (i) { + final dayLabel = _weekDayLabels[i]; + return Container( + height: cellSize, + alignment: AlignmentDirectional.centerStart, + margin: const EdgeInsetsDirectional.all(0.5).add( + const EdgeInsetsDirectional.only(end: 4), + ), + padding: const EdgeInsets.only(right: 6), + child: Text( + dayLabel, + textAlign: TextAlign.start, + style: context.textTheme.bodySmall?.copyWith( + color: textColor, + fontSize: 8, + fontWeight: FontWeight.w500, + ), + ), + ); + }), + ); + } +} diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_gradient.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_gradient.dart new file mode 100644 index 00000000..76a1d2bf --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_gradient.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class OccupancyHeatMapGradient extends StatelessWidget { + const OccupancyHeatMapGradient({super.key, required this.maxValue}); + + final int maxValue; + List _heatMapColors() { + if (maxValue == 0) { + return [ + ColorsManager.vividBlue.withValues(alpha: 0), + ColorsManager.vividBlue.withValues(alpha: 0), + ]; + } + return List.generate( + maxValue + 1, + (index) => ColorsManager.vividBlue.withValues(alpha: index / maxValue), + ); + } + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const Spacer(), + Tooltip( + message: 'Min: 0 - Max: $maxValue', + child: Container( + width: 150, + height: 20, + decoration: BoxDecoration( + border: Border.all( + color: ColorsManager.grayBorder, + width: 1, + ), + gradient: LinearGradient( + begin: AlignmentDirectional.centerStart, + end: AlignmentDirectional.centerEnd, + colors: _heatMapColors(), + ), + ), + ), + ), + ], + ); + } +} diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_months.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_months.dart new file mode 100644 index 00000000..cbb01a7b --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_months.dart @@ -0,0 +1,60 @@ +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:syncrow_web/pages/analytics/modules/occupancy/widgets/occupancy_heat_map_days.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class OccupancyHeatMapMonths extends StatelessWidget { + const OccupancyHeatMapMonths({ + required this.startDate, + required this.cellSize, + super.key, + }); + + final DateTime startDate; + final double cellSize; + + @override + Widget build(BuildContext context) { + return Container( + height: 48, + width: double.infinity, + color: Colors.transparent, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + OccupancyHeatMapDays( + cellSize: cellSize / 3, + textColor: Colors.transparent, + ), + ...List.generate(12, (monthIndex) { + final monthStartDate = DateTime(startDate.year, monthIndex + 1, 1); + final monthName = DateFormat.MMM().format(monthStartDate); + return Expanded( + child: RotatedBox( + quarterTurns: 3, + child: Container( + padding: EdgeInsetsDirectional.zero, + margin: EdgeInsetsDirectional.zero, + decoration: const BoxDecoration( + border: Border( + top: BorderSide(color: ColorsManager.borderColor), + ), + ), + width: cellSize * 4, + child: Padding( + padding: const EdgeInsets.only(left: 4, top: 2), + child: Text( + monthName, + style: const TextStyle(fontSize: 8), + ), + ), + ), + ), + ); + }), + ], + ), + ); + } +} diff --git a/lib/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart b/lib/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart new file mode 100644 index 00000000..b654d64e --- /dev/null +++ b/lib/pages/analytics/modules/occupancy/widgets/occupancy_painter.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +class OccupancyPaintItem { + final int index; + final int value; + final DateTime date; + + const OccupancyPaintItem({ + required this.index, + required this.value, + required this.date, + }); +} + +class OccupancyPainter extends CustomPainter { + OccupancyPainter({ + required this.items, + required this.maxValue, + this.hoveredItem, + }); + + final List items; + final int maxValue; + final OccupancyPaintItem? hoveredItem; + + static const double cellSize = 16.0; + + @override + void paint(Canvas canvas, Size size) { + final fillPaint = Paint(); + final borderPaint = Paint() + ..color = ColorsManager.grayBorder.withValues(alpha: 0.4) + ..style = PaintingStyle.stroke; + final hoveredBorderPaint = Paint() + ..color = Colors.black + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5; + + for (final item in items) { + final column = item.index ~/ 7; + final row = item.index % 7; + + final x = column * cellSize; + final y = row * cellSize; + + fillPaint.color = _getColor(item.value); + final rect = Rect.fromLTWH(x, y, cellSize, cellSize); + canvas.drawRect(rect, fillPaint); + + if (hoveredItem != null && hoveredItem!.index == item.index) { + canvas.drawRect(rect, hoveredBorderPaint); + } else { + _drawDashedLine( + canvas, + Offset(x, y), + Offset(x + cellSize, y), + borderPaint, + ); + _drawDashedLine( + canvas, + Offset(x, y + cellSize), + Offset(x + cellSize, y + cellSize), + borderPaint, + ); + + canvas.drawLine(Offset(x, y), Offset(x, y + cellSize), borderPaint); + canvas.drawLine(Offset(x + cellSize, y), Offset(x + cellSize, y + cellSize), + borderPaint); + } + } + } + + void _drawDashedLine(Canvas canvas, Offset start, Offset end, Paint paint) { + const dashWidth = 2.0; + const dashSpace = 4.0; + final totalLength = (end - start).distance; + final direction = (end - start) / (end - start).distance; + + var currentLength = 0.0; + while (currentLength < totalLength) { + final dashStart = start + direction * currentLength; + final nextLength = currentLength + dashWidth; + final dashEnd = + start + direction * (nextLength < totalLength ? nextLength : totalLength); + canvas.drawLine(dashStart, dashEnd, paint); + currentLength = nextLength + dashSpace; + } + } + + Color _getColor(int value) { + if (maxValue == 0) return ColorsManager.vividBlue.withValues(alpha: 0); + final clampedValue = 0.075 + (1 * value.clamp(0, maxValue) / maxValue); + final opacity = value == 0 ? 0 : clampedValue; + return ColorsManager.vividBlue.withValues(alpha: opacity.toDouble()); + } + + @override + bool shouldRepaint(covariant OccupancyPainter oldDelegate) => + oldDelegate.hoveredItem != hoveredItem; +} diff --git a/lib/pages/analytics/params/get_air_quality_distribution_param.dart b/lib/pages/analytics/params/get_air_quality_distribution_param.dart new file mode 100644 index 00000000..ecad66b0 --- /dev/null +++ b/lib/pages/analytics/params/get_air_quality_distribution_param.dart @@ -0,0 +1,14 @@ +import 'package:syncrow_web/pages/analytics/modules/air_quality/widgets/aqi_type_dropdown.dart'; + +class GetAirQualityDistributionParam { + final DateTime date; + final String spaceUuid; + final AqiType aqiType; + + const GetAirQualityDistributionParam( + { + required this.date, + required this.spaceUuid, + required this.aqiType, + }); +} diff --git a/lib/pages/analytics/params/get_analytics_devices_param.dart b/lib/pages/analytics/params/get_analytics_devices_param.dart new file mode 100644 index 00000000..f8f4e526 --- /dev/null +++ b/lib/pages/analytics/params/get_analytics_devices_param.dart @@ -0,0 +1,22 @@ +enum AnalyticsDeviceRequestType { energyManagement, occupancy } + +class GetAnalyticsDevicesParam { + final String? spaceUuid; + final List deviceTypes; + final String? communityUuid; + final AnalyticsDeviceRequestType requestType; + + const GetAnalyticsDevicesParam({ + required this.requestType, + required this.spaceUuid, + required this.deviceTypes, + required this.communityUuid, + }); + + Map toJson() { + return { + if (spaceUuid != null) 'spaceUuid': spaceUuid, + if (communityUuid != null) 'communityUuid': communityUuid, + }; + } +} diff --git a/lib/pages/analytics/params/get_device_location_data_param.dart b/lib/pages/analytics/params/get_device_location_data_param.dart new file mode 100644 index 00000000..29427d10 --- /dev/null +++ b/lib/pages/analytics/params/get_device_location_data_param.dart @@ -0,0 +1,11 @@ +class GetDeviceLocationDataParam { + const GetDeviceLocationDataParam({ + required this.latitude, + required this.longitude, + }); + + final double latitude; + final double longitude; + + Map toJson() => {'lat': latitude, 'lon': longitude}; +} diff --git a/lib/pages/analytics/params/get_energy_consumption_by_phases_param.dart b/lib/pages/analytics/params/get_energy_consumption_by_phases_param.dart index 169e2753..243e156e 100644 --- a/lib/pages/analytics/params/get_energy_consumption_by_phases_param.dart +++ b/lib/pages/analytics/params/get_energy_consumption_by_phases_param.dart @@ -1,24 +1,20 @@ import 'package:equatable/equatable.dart'; class GetEnergyConsumptionByPhasesParam extends Equatable { - final DateTime? startDate; - final DateTime? endDate; - final String? spaceId; + final String powerClampUuid; + final DateTime? date; const GetEnergyConsumptionByPhasesParam({ - this.startDate, - this.endDate, - this.spaceId, + required this.powerClampUuid, + this.date, }); Map toJson() { return { - 'startDate': startDate?.toIso8601String(), - 'endDate': endDate?.toIso8601String(), - 'spaceId': spaceId, + 'monthDate': '${date?.year}-${date?.month.toString().padLeft(2, '0')}', }; } @override - List get props => [startDate, endDate, spaceId]; + List get props => [powerClampUuid, date]; } diff --git a/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart b/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart index 4995b843..c219893e 100644 --- a/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart +++ b/lib/pages/analytics/params/get_energy_consumption_per_device_param.dart @@ -1,3 +1,16 @@ class GetEnergyConsumptionPerDeviceParam { - const GetEnergyConsumptionPerDeviceParam(); + const GetEnergyConsumptionPerDeviceParam({ + this.monthDate, + this.spaceId, + }); + + final DateTime? monthDate; + final String? spaceId; + + Map toJson() => { + 'monthDate': + '${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}', + if (spaceId != null) 'spaceUuid': spaceId, + 'groupByDevice': true, + }; } diff --git a/lib/pages/analytics/params/get_occupancy_heat_map_param.dart b/lib/pages/analytics/params/get_occupancy_heat_map_param.dart new file mode 100644 index 00000000..0bcad002 --- /dev/null +++ b/lib/pages/analytics/params/get_occupancy_heat_map_param.dart @@ -0,0 +1,13 @@ +class GetOccupancyHeatMapParam { + final DateTime year; + final String spaceUuid; + + const GetOccupancyHeatMapParam({ + required this.year, + required this.spaceUuid, + }); + + Map toJson() { + return {'year': year.year}; + } +} diff --git a/lib/pages/analytics/params/get_occupancy_param.dart b/lib/pages/analytics/params/get_occupancy_param.dart new file mode 100644 index 00000000..b083197b --- /dev/null +++ b/lib/pages/analytics/params/get_occupancy_param.dart @@ -0,0 +1,11 @@ +class GetOccupancyParam { + final String monthDate; + final String? spaceUuid; + + GetOccupancyParam({ + required this.monthDate, + required this.spaceUuid, + }); + + Map toJson() => {'monthDate': monthDate}; +} diff --git a/lib/pages/analytics/params/get_range_of_aqi_param.dart b/lib/pages/analytics/params/get_range_of_aqi_param.dart new file mode 100644 index 00000000..ef53fe76 --- /dev/null +++ b/lib/pages/analytics/params/get_range_of_aqi_param.dart @@ -0,0 +1,14 @@ +import 'package:equatable/equatable.dart'; + +class GetRangeOfAqiParam extends Equatable { + final DateTime date; + final String spaceUuid; + + const GetRangeOfAqiParam({ + required this.date, + required this.spaceUuid, + }); + + @override + List get props => [date, spaceUuid]; +} diff --git a/lib/pages/analytics/params/get_total_energy_consumption_param.dart b/lib/pages/analytics/params/get_total_energy_consumption_param.dart index 47b75cb8..f5615cca 100644 --- a/lib/pages/analytics/params/get_total_energy_consumption_param.dart +++ b/lib/pages/analytics/params/get_total_energy_consumption_param.dart @@ -1,19 +1,18 @@ class GetTotalEnergyConsumptionParam { - final DateTime? startDate; - final DateTime? endDate; + final DateTime? monthDate; final String? spaceId; const GetTotalEnergyConsumptionParam({ - this.startDate, - this.endDate, + this.monthDate, this.spaceId, }); Map toJson() { return { - 'startDate': startDate?.toIso8601String(), - 'endDate': endDate?.toIso8601String(), - 'spaceId': spaceId, + 'monthDate': + '${monthDate?.year}-${monthDate?.month.toString().padLeft(2, '0')}', + if (spaceId != null) 'spaceUuid': spaceId, + 'groupByDevice': false, }; } } diff --git a/lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart new file mode 100644 index 00000000..ef63856a --- /dev/null +++ b/lib/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart @@ -0,0 +1,8 @@ +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; + +abstract interface class AirQualityDistributionService { + Future> getAirQualityDistribution( + GetAirQualityDistributionParam param, + ); +} diff --git a/lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart b/lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart new file mode 100644 index 00000000..49755f7b --- /dev/null +++ b/lib/pages/analytics/services/air_quality_distribution/remote_air_quality_distribution_service.dart @@ -0,0 +1,41 @@ +import 'package:syncrow_web/pages/analytics/models/air_quality_data_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_air_quality_distribution_param.dart'; +import 'package:syncrow_web/pages/analytics/services/air_quality_distribution/air_quality_distribution_service.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +final class RemoteAirQualityDistributionService + implements AirQualityDistributionService { + RemoteAirQualityDistributionService(this._httpService); + + final HTTPService _httpService; + + @override + Future> getAirQualityDistribution( + GetAirQualityDistributionParam param, + ) async { + try { + final response = await _httpService.get( + path: '/aqi/distribution/space/${param.spaceUuid}', + queryParameters: { + 'monthDate': _formatDate(param.date), + 'pollutantType': param.aqiType.code, + }, + expectedResponseModel: (data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as List? ?? []; + return mappedData.map((e) { + final jsonData = e as Map; + return AirQualityDataModel.fromJson(jsonData); + }).toList(); + }, + ); + return response; + } catch (e) { + throw Exception('Failed to load energy consumption per phase: $e'); + } + } + + static String _formatDate(DateTime date) { + return '${date.year}-${date.month.toString().padLeft(2, '0')}'; + } +} diff --git a/lib/pages/analytics/services/analytics_devices/analytics_devices_service.dart b/lib/pages/analytics/services/analytics_devices/analytics_devices_service.dart new file mode 100644 index 00000000..b8bae76a --- /dev/null +++ b/lib/pages/analytics/services/analytics_devices/analytics_devices_service.dart @@ -0,0 +1,6 @@ +import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; +import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; + +abstract interface class AnalyticsDevicesService { + Future> getDevices(GetAnalyticsDevicesParam param); +} diff --git a/lib/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart b/lib/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart new file mode 100644 index 00000000..2d735df6 --- /dev/null +++ b/lib/pages/analytics/services/analytics_devices/analytics_devices_service_delagate.dart @@ -0,0 +1,24 @@ +import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; +import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; +import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart'; + +class AnalyticsDevicesServiceDelegate implements AnalyticsDevicesService { + const AnalyticsDevicesServiceDelegate( + this._occupancyService, + this._energyManagementService, + ); + + final AnalyticsDevicesService _occupancyService; + final AnalyticsDevicesService _energyManagementService; + + @override + Future> getDevices( + GetAnalyticsDevicesParam param, + ) { + return switch (param.requestType) { + AnalyticsDeviceRequestType.occupancy => _occupancyService.getDevices(param), + AnalyticsDeviceRequestType.energyManagement => + _energyManagementService.getDevices(param), + }; + } +} diff --git a/lib/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart b/lib/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart new file mode 100644 index 00000000..9ef711e9 --- /dev/null +++ b/lib/pages/analytics/services/analytics_devices/remote_energy_management_analytics_devices_service.dart @@ -0,0 +1,46 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; +import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; +import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +final class RemoteEnergyManagementAnalyticsDevicesService + implements AnalyticsDevicesService { + const RemoteEnergyManagementAnalyticsDevicesService(this._httpService); + + final HTTPService _httpService; + + static const _defaultErrorMessage = 'Failed to load analytics devices'; + + @override + Future> getDevices(GetAnalyticsDevicesParam param) async { + try { + final response = await _httpService.get( + path: '/devices-space-community/recursive-child', + queryParameters: param.toJson() + ..addAll({'productType': param.deviceTypes.first}), + expectedResponseModel: (response) { + final json = response as Map; + final dailyData = json['data'] as List? ?? []; + + final result = dailyData.map( + (json) => AnalyticsDevice.fromJson(json as Map), + ); + + return result.toList(); + }, + ); + + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + throw APIException('$_defaultErrorMessage: $e'); + } + } +} diff --git a/lib/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart b/lib/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart new file mode 100644 index 00000000..736b0804 --- /dev/null +++ b/lib/pages/analytics/services/analytics_devices/remote_occupancy_analytics_devices_service.dart @@ -0,0 +1,78 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; +import 'package:syncrow_web/pages/analytics/params/get_analytics_devices_param.dart'; +import 'package:syncrow_web/pages/analytics/services/analytics_devices/analytics_devices_service.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +class RemoteOccupancyAnalyticsDevicesService implements AnalyticsDevicesService { + const RemoteOccupancyAnalyticsDevicesService(this._httpService); + + final HTTPService _httpService; + + static const _defaultErrorMessage = 'Failed to load analytics devices'; + + @override + Future> getDevices(GetAnalyticsDevicesParam param) async { + try { + final requests = await Future.wait>( + param.deviceTypes.map((e) { + final mappedParam = GetAnalyticsDevicesParam( + requestType: AnalyticsDeviceRequestType.occupancy, + spaceUuid: param.spaceUuid, + deviceTypes: [e], + communityUuid: param.communityUuid, + ); + return _makeRequest(mappedParam); + }).toList(), + ); + + final result = requests.map((e) => e.first).toList(); + return result; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + final formattedErrorMessage = [_defaultErrorMessage, e.toString()].join(': '); + throw APIException(formattedErrorMessage); + } + } + + Future> _makeRequest(GetAnalyticsDevicesParam param) async { + try { + final projectUuid = await ProjectManager.getProjectUUID(); + + final response = await _httpService.get( + path: + '/projects/$projectUuid/communities/${param.communityUuid}/spaces/${param.spaceUuid}/devices', + queryParameters: { + 'communityUuid': param.communityUuid, + 'spaceUuid': param.spaceUuid, + 'productType': param.deviceTypes.first, + }, + expectedResponseModel: (response) { + final json = response as Map; + final dailyData = json['data'] as List? ?? []; + + final result = dailyData.map( + (json) => AnalyticsDevice.fromJson(json as Map), + ); + return result.toList(); + }, + ); + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + throw APIException('$_defaultErrorMessage: $e'); + } + } +} diff --git a/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart b/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart new file mode 100644 index 00000000..f38f607d --- /dev/null +++ b/lib/pages/analytics/services/device_location/device_location_details_service_decorator.dart @@ -0,0 +1,40 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/analytics/models/device_location_info.dart'; +import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; +import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart'; + +class DeviceLocationDetailsServiceDecorator implements DeviceLocationService { + const DeviceLocationDetailsServiceDecorator(this._decoratee, this._dio); + + final DeviceLocationService _decoratee; + final Dio _dio; + + @override + Future get(GetDeviceLocationDataParam param) async { + try { + final deviceLocationInfo = await _decoratee.get(param); + final response = await _dio.get>( + 'reverse', + queryParameters: { + 'format': 'json', + 'lat': param.latitude, + 'lon': param.longitude, + }, + ); + + final data = response.data; + if (data != null) { + final addressData = data['address'] as Map; + return deviceLocationInfo.copyWith( + city: addressData['city'] as String?, + country: addressData['country_code']?.toString().toUpperCase(), + address: addressData['state'] as String?, + ); + } + + return deviceLocationInfo; + } catch (e) { + throw Exception('Failed to load device location info: $e'); + } + } +} diff --git a/lib/pages/analytics/services/device_location/device_location_service.dart b/lib/pages/analytics/services/device_location/device_location_service.dart new file mode 100644 index 00000000..d28b4a7b --- /dev/null +++ b/lib/pages/analytics/services/device_location/device_location_service.dart @@ -0,0 +1,6 @@ +import 'package:syncrow_web/pages/analytics/models/device_location_info.dart'; +import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; + +abstract interface class DeviceLocationService { + Future get(GetDeviceLocationDataParam param); +} diff --git a/lib/pages/analytics/services/device_location/remote_device_location_service.dart b/lib/pages/analytics/services/device_location/remote_device_location_service.dart new file mode 100644 index 00000000..b8820180 --- /dev/null +++ b/lib/pages/analytics/services/device_location/remote_device_location_service.dart @@ -0,0 +1,37 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/analytics/models/device_location_info.dart'; +import 'package:syncrow_web/pages/analytics/params/get_device_location_data_param.dart'; +import 'package:syncrow_web/pages/analytics/services/device_location/device_location_service.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +class RemoteDeviceLocationService implements DeviceLocationService { + const RemoteDeviceLocationService(this._httpService); + + final HTTPService _httpService; + + static const _defaultErrorMessage = 'Failed to load device location'; + + @override + Future get(GetDeviceLocationDataParam param) async { + try { + final response = await _httpService.get( + path: '/weather', + queryParameters: param.toJson(), + expectedResponseModel: (data) { + final response = data as Map; + final location = response['data'] as Map; + + return DeviceLocationInfo.fromJson(location); + }, + ); + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + throw Exception(errorMessage); + } catch (e) { + throw Exception('$_defaultErrorMessage: $e'); + } + } +} diff --git a/lib/pages/analytics/services/energy_consumption_by_phases/fake_energy_consumption_by_phases_service.dart b/lib/pages/analytics/services/energy_consumption_by_phases/fake_energy_consumption_by_phases_service.dart deleted file mode 100644 index f6ce67c9..00000000 --- a/lib/pages/analytics/services/energy_consumption_by_phases/fake_energy_consumption_by_phases_service.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart'; -import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart'; -import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart'; - -class FakeEnergyConsumptionByPhasesService - implements EnergyConsumptionByPhasesService { - @override - Future> load( - GetEnergyConsumptionByPhasesParam param, - ) { - return Future.delayed( - const Duration(milliseconds: 500), - () => const [ - PhasesEnergyConsumption(month: 1, phaseA: 200, phaseB: 300, phaseC: 400), - PhasesEnergyConsumption(month: 2, phaseA: 300, phaseB: 400, phaseC: 500), - PhasesEnergyConsumption(month: 3, phaseA: 400, phaseB: 500, phaseC: 600), - PhasesEnergyConsumption(month: 4, phaseA: 100, phaseB: 100, phaseC: 100), - PhasesEnergyConsumption(month: 5, phaseA: 300, phaseB: 400, phaseC: 500), - PhasesEnergyConsumption(month: 6, phaseA: 300, phaseB: 100, phaseC: 400), - PhasesEnergyConsumption(month: 7, phaseA: 300, phaseB: 100, phaseC: 400), - PhasesEnergyConsumption(month: 8, phaseA: 500, phaseB: 100, phaseC: 100), - PhasesEnergyConsumption(month: 9, phaseA: 500, phaseB: 100, phaseC: 200), - PhasesEnergyConsumption(month: 10, phaseA: 100, phaseB: 50, phaseC: 50), - PhasesEnergyConsumption(month: 11, phaseA: 600, phaseB: 750, phaseC: 130), - PhasesEnergyConsumption(month: 12, phaseA: 100, phaseB: 100, phaseC: 100), - ], - ); - } -} diff --git a/lib/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart b/lib/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart index f0ac31ed..17f9baff 100644 --- a/lib/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart +++ b/lib/pages/analytics/services/energy_consumption_by_phases/remote_energy_consumption_by_phases_service.dart @@ -1,6 +1,8 @@ +import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/analytics/models/phases_energy_consumption.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_by_phases_param.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_by_phases/energy_consumption_by_phases_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; final class RemoteEnergyConsumptionByPhasesService @@ -9,14 +11,17 @@ final class RemoteEnergyConsumptionByPhasesService final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load energy consumption per phase'; + @override Future> load( GetEnergyConsumptionByPhasesParam param, ) async { try { final response = await _httpService.get( - path: 'endpoint', + path: '/power-clamp/${param.powerClampUuid}/historical', showServerMessage: true, + queryParameters: param.toJson(), expectedResponseModel: (data) { final json = data as Map? ?? {}; final mappedData = json['data'] as List? ?? []; @@ -27,8 +32,15 @@ final class RemoteEnergyConsumptionByPhasesService }, ); return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); } catch (e) { - throw Exception('Failed to load energy consumption per device: $e'); + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); } } } diff --git a/lib/pages/analytics/services/energy_consumption_per_device/fake_energy_consumption_per_device_service.dart b/lib/pages/analytics/services/energy_consumption_per_device/fake_energy_consumption_per_device_service.dart deleted file mode 100644 index b1608eea..00000000 --- a/lib/pages/analytics/services/energy_consumption_per_device/fake_energy_consumption_per_device_service.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'dart:math' as math show Random; - -import 'package:flutter/material.dart'; -import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart'; -import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart'; -import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart'; -import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart'; - -class FakeEnergyConsumptionPerDeviceService - implements EnergyConsumptionPerDeviceService { - @override - Future> load( - GetEnergyConsumptionPerDeviceParam param, - ) { - final random = math.Random(); - return Future.delayed(const Duration(milliseconds: 500), () { - return [ - (Colors.redAccent, 1), - (Colors.lightBlueAccent, 2), - (Colors.purpleAccent, 3), - ].map((e) { - final (color, index) = e; - return DeviceEnergyDataModel( - color: color, - energy: List.generate(30, (i) => i) - .map( - (index) => EnergyDataModel( - date: DateTime(2025, 1, index + 1), - value: random.nextInt(100) + (index * 100), - ), - ) - .toList(), - deviceName: 'Device $index', - deviceId: 'device_$index', - ); - }).toList(); - }); - } -} diff --git a/lib/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart b/lib/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart index 2c43bb23..82b21b1c 100644 --- a/lib/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart +++ b/lib/pages/analytics/services/energy_consumption_per_device/remote_energy_consumption_per_device_service.dart @@ -1,6 +1,10 @@ +import 'package:dio/dio.dart'; +import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/analytics/models/device_energy_data_model.dart'; +import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/params/get_energy_consumption_per_device_param.dart'; import 'package:syncrow_web/pages/analytics/services/energy_consumption_per_device/energy_consumption_per_device_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; class RemoteEnergyConsumptionPerDeviceService @@ -9,26 +13,56 @@ class RemoteEnergyConsumptionPerDeviceService final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load energy consumption per device'; + @override Future> load( GetEnergyConsumptionPerDeviceParam param, ) async { try { final response = await _httpService.get( - path: 'endpoint', + path: '/power-clamp/historical', showServerMessage: true, - expectedResponseModel: (data) { - final json = data as Map? ?? {}; - final mappedData = json['data'] as List? ?? []; - return mappedData.map((e) { - final jsonData = e as Map; - return DeviceEnergyDataModel.fromJson(jsonData); - }).toList(); - }, + queryParameters: param.toJson(), + expectedResponseModel: _EnergyConsumptionPerDeviceMapper.map, ); return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); } catch (e) { - throw Exception('Failed to load energy consumption per device: $e'); + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); } } } + +abstract final class _EnergyConsumptionPerDeviceMapper { + const _EnergyConsumptionPerDeviceMapper._(); + static List map(dynamic data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as List? ?? []; + return mappedData.map((e) { + final deviceData = e as Map; + final energyData = deviceData['data'] as List; + + return DeviceEnergyDataModel( + deviceId: deviceData['deviceUuid'] as String, + deviceName: deviceData['deviceName'] as String, + color: Color((DateTime.now().microsecondsSinceEpoch + + deviceData['deviceUuid'].hashCode) | + 0xFF000000), + energy: energyData.map((data) { + final energyJson = data as Map; + return EnergyDataModel( + date: DateTime.parse(energyJson['date'] as String), + value: double.parse(energyJson['total_energy_consumed_kw'] as String), + ); + }).toList(), + ); + }).toList(); + } +} diff --git a/lib/pages/analytics/services/occupacy/occupacy_service.dart b/lib/pages/analytics/services/occupacy/occupacy_service.dart new file mode 100644 index 00000000..ced7d360 --- /dev/null +++ b/lib/pages/analytics/services/occupacy/occupacy_service.dart @@ -0,0 +1,6 @@ +import 'package:syncrow_web/pages/analytics/models/occupacy.dart'; +import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart'; + +abstract interface class OccupacyService { + Future> load(GetOccupancyParam param); +} diff --git a/lib/pages/analytics/services/occupacy/remote_occupancy_service.dart b/lib/pages/analytics/services/occupacy/remote_occupancy_service.dart new file mode 100644 index 00000000..afd3f79e --- /dev/null +++ b/lib/pages/analytics/services/occupacy/remote_occupancy_service.dart @@ -0,0 +1,43 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/analytics/models/occupacy.dart'; +import 'package:syncrow_web/pages/analytics/params/get_occupancy_param.dart'; +import 'package:syncrow_web/pages/analytics/services/occupacy/occupacy_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +final class RemoteOccupancyService implements OccupacyService { + const RemoteOccupancyService(this._httpService); + + final HTTPService _httpService; + + static const _defaultErrorMessage = 'Failed to load occupancy'; + + @override + Future> load(GetOccupancyParam param) async { + try { + final response = await _httpService.get( + path: '/occupancy/duration/space/${param.spaceUuid}', + showServerMessage: true, + queryParameters: param.toJson(), + expectedResponseModel: (data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as List? ?? []; + return mappedData.map((e) { + final jsonData = e as Map; + return Occupacy.fromJson(jsonData); + }).toList(); + }, + ); + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); + } + } +} diff --git a/lib/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart b/lib/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart new file mode 100644 index 00000000..1a54c0a9 --- /dev/null +++ b/lib/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart @@ -0,0 +1,6 @@ +import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart'; + +abstract interface class OccupancyHeatMapService { + Future> load(GetOccupancyHeatMapParam param); +} diff --git a/lib/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart b/lib/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart new file mode 100644 index 00000000..0d7f6500 --- /dev/null +++ b/lib/pages/analytics/services/occupancy_heat_map/remote_occupancy_heat_map_service.dart @@ -0,0 +1,46 @@ +import 'package:dio/dio.dart'; +import 'package:syncrow_web/pages/analytics/models/occupancy_heat_map_model.dart'; +import 'package:syncrow_web/pages/analytics/params/get_occupancy_heat_map_param.dart'; +import 'package:syncrow_web/pages/analytics/services/occupancy_heat_map/occupancy_heat_map_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +final class RemoteOccupancyHeatMapService implements OccupancyHeatMapService { + const RemoteOccupancyHeatMapService(this._httpService); + + final HTTPService _httpService; + + static const _defaultErrorMessage = 'Failed to load occupancy heat map'; + + @override + Future> load(GetOccupancyHeatMapParam param) async { + try { + final response = await _httpService.get( + path: '/occupancy/heat-map/space/${param.spaceUuid}', + showServerMessage: true, + queryParameters: param.toJson(), + expectedResponseModel: (response) { + final json = response as Map; + final dailyData = json['data'] as List? ?? []; + + final result = dailyData.map( + (json) => OccupancyHeatMapModel.fromJson(json as Map), + ); + + return result.toList(); + }, + ); + + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); + } catch (e) { + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); + } + } +} diff --git a/lib/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart b/lib/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart index 17d5a7fc..b4bc82c6 100644 --- a/lib/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart +++ b/lib/pages/analytics/services/power_clamp_info/remote_power_clamp_info_service.dart @@ -1,5 +1,7 @@ +import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/analytics/services/power_clamp_info/power_clamp_info_service.dart'; import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_model.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; final class RemotePowerClampInfoService implements PowerClampInfoService { @@ -7,6 +9,8 @@ final class RemotePowerClampInfoService implements PowerClampInfoService { final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to fetch power clamp info'; + @override Future getInfo(String deviceId) async { try { @@ -20,8 +24,15 @@ final class RemotePowerClampInfoService implements PowerClampInfoService { }, ); return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); } catch (e) { - throw Exception('Failed to fetch power clamp info: $e'); + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); } } } diff --git a/lib/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart b/lib/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart new file mode 100644 index 00000000..9e1657e3 --- /dev/null +++ b/lib/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart @@ -0,0 +1,6 @@ +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; + +abstract interface class RangeOfAqiService { + Future> load(GetRangeOfAqiParam param); +} \ No newline at end of file diff --git a/lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart b/lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart new file mode 100644 index 00000000..642ad400 --- /dev/null +++ b/lib/pages/analytics/services/range_of_aqi/remote_range_of_aqi_service.dart @@ -0,0 +1,35 @@ +import 'package:syncrow_web/pages/analytics/models/range_of_aqi.dart'; +import 'package:syncrow_web/pages/analytics/params/get_range_of_aqi_param.dart'; +import 'package:syncrow_web/pages/analytics/services/range_of_aqi/range_of_aqi_service.dart'; +import 'package:syncrow_web/services/api/http_service.dart'; + +final class RemoteRangeOfAqiService implements RangeOfAqiService { + const RemoteRangeOfAqiService(this._httpService); + + final HTTPService _httpService; + + @override + Future> load(GetRangeOfAqiParam param) async { + try { + final response = await _httpService.get( + path: '/aqi/range/space/${param.spaceUuid}', + queryParameters: {'monthDate': _formatDate(param.date)}, + expectedResponseModel: (data) { + final json = data as Map? ?? {}; + final mappedData = json['data'] as List? ?? []; + return mappedData.map((e) { + final jsonData = e as Map; + return RangeOfAqi.fromJson(jsonData); + }).toList(); + }, + ); + return response; + } catch (e) { + throw Exception('Failed to load range of aqi: $e'); + } + } + + static String _formatDate(DateTime date) { + return '${date.year}-${date.month.toString().padLeft(2, '0')}'; + } +} diff --git a/lib/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart b/lib/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart index 2fd379e4..4080e89b 100644 --- a/lib/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart +++ b/lib/pages/analytics/services/realtime_device_service/firebase_realtime_device_service.dart @@ -23,7 +23,7 @@ class FirebaseRealtimeDeviceService implements RealtimeDeviceService { return Status( code: status['code']?.toString() ?? '', - value: num.tryParse(status['value']?.toString() ?? '0'), + value: status['value']?.toString() ?? '', ); }).toList(); }); diff --git a/lib/pages/analytics/services/total_energy_consumption/fake_total_energy_consumption_service.dart b/lib/pages/analytics/services/total_energy_consumption/fake_total_energy_consumption_service.dart deleted file mode 100644 index a4a8a62d..00000000 --- a/lib/pages/analytics/services/total_energy_consumption/fake_total_energy_consumption_service.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart'; -import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart'; -import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart'; - -class FakeTotalEnergyConsumptionService implements TotalEnergyConsumptionService { - @override - Future> load( - GetTotalEnergyConsumptionParam param, - ) { - return Future.value( - List.generate(30, (index) { - return EnergyDataModel( - date: DateTime(2025, 1, index + 1), - value: 20000 + (index * 1000) % 5000, - ); - }), - ); - } -} diff --git a/lib/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart b/lib/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart index 193404ca..838cc5e7 100644 --- a/lib/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart +++ b/lib/pages/analytics/services/total_energy_consumption/remote_total_energy_consumption_service.dart @@ -1,6 +1,8 @@ +import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/analytics/models/energy_data_model.dart'; import 'package:syncrow_web/pages/analytics/params/get_total_energy_consumption_param.dart'; import 'package:syncrow_web/pages/analytics/services/total_energy_consumption/total_energy_consumption_service.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionService { @@ -8,26 +10,52 @@ class RemoteTotalEnergyConsumptionService implements TotalEnergyConsumptionServi final HTTPService _httpService; + static const _defaultErrorMessage = 'Failed to load total energy consumption'; + @override Future> load( GetTotalEnergyConsumptionParam param, ) async { try { final response = await _httpService.get( - path: 'endpoint', + path: '/power-clamp/historical', showServerMessage: true, - expectedResponseModel: (data) { - final json = data as Map? ?? {}; - final mappedData = json['data'] as List? ?? []; - return mappedData.map((e) { - final jsonData = e as Map; - return EnergyDataModel.fromJson(jsonData); - }).toList(); - }, + queryParameters: param.toJson(), + expectedResponseModel: _TotalEnergyConsumptionResponseMapper.map, ); + return response; + } on DioException catch (e) { + final message = e.response?.data as Map?; + final error = message?['error'] as Map?; + final errorMessage = error?['error'] as String? ?? ''; + final formattedErrorMessage = [_defaultErrorMessage, errorMessage].join(': '); + throw APIException(formattedErrorMessage); } catch (e) { - throw Exception('Failed to load total energy consumption: $e'); + final formattedErrorMessage = [_defaultErrorMessage, '$e'].join(': '); + throw APIException(formattedErrorMessage); } } } + +abstract final class _TotalEnergyConsumptionResponseMapper { + const _TotalEnergyConsumptionResponseMapper._(); + + static List map(dynamic data) { + final json = data as Map? ?? {}; + final dailyData = json['data'] as List? ?? []; + + return dailyData.map((dayData) { + final date = dayData['date'] as String; + final energyValue = double.tryParse( + dayData['total_energy_consumed_kw'] as String? ?? '0', + ) ?? + 0.0; + + return EnergyDataModel( + date: DateTime.parse(date), + value: energyValue, + ); + }).toList(); + } +} diff --git a/lib/pages/analytics/widgets/analytics_error_widget.dart b/lib/pages/analytics/widgets/analytics_error_widget.dart index 354eb31a..7c560da4 100644 --- a/lib/pages/analytics/widgets/analytics_error_widget.dart +++ b/lib/pages/analytics/widgets/analytics_error_widget.dart @@ -11,14 +11,17 @@ class AnalyticsErrorWidget extends StatelessWidget { Widget build(BuildContext context) { return Visibility( visible: errorMessage != null || (errorMessage?.isNotEmpty ?? false), - child: Text( - '$errorMessage ?? "Something went wrong"', - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.red, - fontWeight: FontWeight.w400, - fontSize: 8, + child: Padding( + padding: const EdgeInsetsDirectional.only(bottom: 10), + child: Text( + errorMessage ?? 'Something went wrong', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.red, + fontWeight: FontWeight.w400, + fontSize: 8, + ), ), ), ); diff --git a/lib/pages/analytics/widgets/analytics_sidebar_header.dart b/lib/pages/analytics/widgets/analytics_sidebar_header.dart new file mode 100644 index 00000000..5ff1d042 --- /dev/null +++ b/lib/pages/analytics/widgets/analytics_sidebar_header.dart @@ -0,0 +1,91 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/analytics/models/analytics_device.dart'; +import 'package:syncrow_web/pages/analytics/modules/analytics/blocs/analytics_devices/analytics_devices_bloc.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/helpers/fetch_energy_management_data_helper.dart'; +import 'package:syncrow_web/pages/analytics/modules/energy_management/widgets/analytics_device_dropdown.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class AnalyticsSidebarHeader extends StatelessWidget { + const AnalyticsSidebarHeader({ + required this.title, + this.showSpaceUuidInDevicesDropdown = false, + this.onChanged, + super.key, + }); + + final String title; + final bool showSpaceUuidInDevicesDropdown; + final void Function(AnalyticsDevice device)? onChanged; + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + flex: 2, + child: FittedBox( + alignment: AlignmentDirectional.centerStart, + fit: BoxFit.scaleDown, + child: SelectableText( + title, + style: context.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w700, + color: ColorsManager.vividBlue.withValues(alpha: 0.6), + fontSize: 18, + ), + ), + ), + ), + const Spacer(), + Expanded( + flex: 2, + child: FittedBox( + alignment: AlignmentDirectional.centerEnd, + fit: BoxFit.scaleDown, + child: AnalyticsDeviceDropdown( + showSpaceUuid: showSpaceUuidInDevicesDropdown, + onChanged: (value) { + context.read().add( + SelectAnalyticsDeviceEvent(value), + ); + FetchEnergyManagementDataHelper.loadRealtimeDeviceChanges( + context, + deviceUuid: value.uuid, + ); + onChanged?.call(value); + }, + ), + ), + ), + ], + ), + Text( + 'Device ID:', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.textPrimaryColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + const SizedBox(height: 6), + SelectableText( + context.watch().state.selectedDevice?.uuid ?? 'N/A', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.blackColor, + fontWeight: FontWeight.w400, + fontSize: 12, + ), + ), + const SizedBox(height: 10), + const Divider(height: 1, color: ColorsManager.greyColor), + const SizedBox(height: 24), + ], + ); + } +} diff --git a/lib/pages/analytics/widgets/charts_loading_widget.dart b/lib/pages/analytics/widgets/charts_loading_widget.dart index cf81fee7..7c59cd88 100644 --- a/lib/pages/analytics/widgets/charts_loading_widget.dart +++ b/lib/pages/analytics/widgets/charts_loading_widget.dart @@ -5,6 +5,7 @@ class ChartsLoadingWidget extends StatelessWidget { required this.isLoading, super.key, }); + final bool isLoading; @override diff --git a/lib/pages/auth/bloc/auth_bloc.dart b/lib/pages/auth/bloc/auth_bloc.dart index 35663557..58950089 100644 --- a/lib/pages/auth/bloc/auth_bloc.dart +++ b/lib/pages/auth/bloc/auth_bloc.dart @@ -13,6 +13,7 @@ import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; import 'package:syncrow_web/pages/home/bloc/home_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/auth_api.dart'; import 'package:syncrow_web/utils/constants/strings_manager.dart'; import 'package:syncrow_web/utils/helpers/shared_preferences_helper.dart'; @@ -99,7 +100,8 @@ class AuthBloc extends Bloc { emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); } - Future changePassword(ChangePasswordEvent event, Emitter emit) async { +Future changePassword( + ChangePasswordEvent event, Emitter emit) async { emit(LoadingForgetState()); try { var response = await AuthenticationAPI.verifyOtp( @@ -113,14 +115,14 @@ class AuthBloc extends Bloc { emit(const TimerState(isButtonEnabled: true, remainingTime: 0)); emit(SuccessForgetState()); } - } on DioException catch (e) { - final errorData = e.response!.data; - String errorMessage = errorData['error']['message'] ?? 'something went wrong'; + } on APIException catch (e) { + final errorMessage = e.message; validate = errorMessage; emit(AuthInitialState()); } } + String? validateCode(String? value) { if (value == null || value.isEmpty) { return 'Code is required'; @@ -149,6 +151,7 @@ class AuthBloc extends Bloc { static UserModel? user; bool showValidationMessage = false; + void _login(LoginButtonPressed event, Emitter emit) async { emit(AuthLoading()); if (isChecked) { @@ -161,25 +164,24 @@ class AuthBloc extends Bloc { token = await AuthenticationAPI.loginWithEmail( model: LoginWithEmailModel( - email: event.username, + email: event.username.toLowerCase(), password: event.password, ), ); - } on DioException catch (e) { - final errorData = e.response!.data; - String errorMessage = errorData['error']['message']; - if (errorMessage == "Access denied for web platform") { - validate = errorMessage; - } else { - validate = 'Invalid Credentials!'; - } + } on APIException catch (e) { + validate = e.message; + emit(LoginInitial()); + return; + } catch (e) { + validate = 'Something went wrong'; emit(LoginInitial()); return; } if (token.accessTokenIsNotEmpty) { FlutterSecureStorage storage = const FlutterSecureStorage(); - await storage.write(key: Token.loginAccessTokenKey, value: token.accessToken); + await storage.write( + key: Token.loginAccessTokenKey, value: token.accessToken); const FlutterSecureStorage().write( key: UserModel.userUuidKey, value: Token.decodeToken(token.accessToken)['uuid'].toString()); @@ -195,6 +197,7 @@ class AuthBloc extends Bloc { } } + checkBoxToggle( CheckBoxEvent event, Emitter emit, diff --git a/lib/pages/common/custom_table.dart b/lib/pages/common/custom_table.dart index 60abc0d2..f23daa45 100644 --- a/lib/pages/common/custom_table.dart +++ b/lib/pages/common/custom_table.dart @@ -21,6 +21,7 @@ class DynamicTable extends StatefulWidget { final List? initialSelectedIds; final int uuidIndex; final Function(dynamic selectedRows)? onSelectionChanged; + final Function(int rowIndex)? onSettingsPressed; const DynamicTable({ super.key, required this.headers, @@ -37,6 +38,7 @@ class DynamicTable extends StatefulWidget { this.initialSelectedIds, required this.uuidIndex, this.onSelectionChanged, + this.onSettingsPressed, }); @override @@ -48,11 +50,20 @@ class _DynamicTableState extends State { bool _selectAll = false; final ScrollController _verticalScrollController = ScrollController(); final ScrollController _horizontalScrollController = ScrollController(); - + late ScrollController _horizontalHeaderScrollController; + late ScrollController _horizontalBodyScrollController; @override void initState() { super.initState(); _initializeSelection(); + _horizontalHeaderScrollController = ScrollController(); + _horizontalBodyScrollController = ScrollController(); + + // Synchronize horizontal scrolling + _horizontalBodyScrollController.addListener(() { + _horizontalHeaderScrollController + .jumpTo(_horizontalBodyScrollController.offset); + }); } @override @@ -63,7 +74,8 @@ class _DynamicTableState extends State { } } - bool _compareListOfLists(List> oldList, List> newList) { + bool _compareListOfLists( + List> oldList, List> newList) { // Check if the old and new lists are the same if (oldList.length != newList.length) return false; @@ -101,87 +113,90 @@ class _DynamicTableState extends State { context.read().add(UpdateSelection(_selectedRows)); } + @override + void dispose() { + _horizontalHeaderScrollController.dispose(); + _horizontalBodyScrollController.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Container( decoration: widget.cellDecoration, - child: Scrollbar( - controller: _verticalScrollController, - thumbVisibility: true, - trackVisibility: true, - child: Scrollbar( - controller: _horizontalScrollController, - thumbVisibility: false, - trackVisibility: false, - notificationPredicate: (notif) => notif.depth == 1, - child: SingleChildScrollView( - controller: _verticalScrollController, + child: Column( + children: [ + Container( + decoration: widget.headerDecoration ?? + const BoxDecoration(color: ColorsManager.boxColor), child: SingleChildScrollView( - controller: _horizontalScrollController, scrollDirection: Axis.horizontal, + physics: const NeverScrollableScrollPhysics(), + controller: _horizontalHeaderScrollController, child: SizedBox( width: widget.size.width, - child: Column( + child: Row( children: [ - Container( - decoration: widget.headerDecoration ?? - const BoxDecoration( - color: ColorsManager.boxColor, - ), - child: Row( - children: [ - if (widget.withCheckBox) _buildSelectAllCheckbox(), - ...List.generate(widget.headers.length, (index) { - return _buildTableHeaderCell(widget.headers[index], index); - }) - //...widget.headers.map((header) => _buildTableHeaderCell(header)), - ], - ), - ), - widget.isEmpty - ? Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Column( - children: [ - SvgPicture.asset(Assets.emptyTable), - const SizedBox( - height: 15, - ), - Text( - widget.tableName == 'AccessManagement' ? 'No Password ' : 'No Devices', - style: Theme.of(context) - .textTheme - .bodySmall! - .copyWith(color: ColorsManager.grayColor), - ) - ], - ), - ], - ), - ], - ) - : Column( - children: List.generate(widget.data.length, (index) { - final row = widget.data[index]; - return Row( - children: [ - if (widget.withCheckBox) _buildRowCheckbox(index, widget.size.height * 0.08), - ...row.map((cell) => _buildTableCell(cell.toString(), widget.size.height * 0.08)), - ], - ); - }), - ), + if (widget.withCheckBox) _buildSelectAllCheckbox(), + ...List.generate(widget.headers.length, (index) { + return _buildTableHeaderCell( + widget.headers[index], index); + }), ], ), ), ), ), - ), + Expanded( + child: Scrollbar( + controller: _verticalScrollController, + thumbVisibility: true, + trackVisibility: true, + child: SingleChildScrollView( + controller: _verticalScrollController, + child: Scrollbar( + controller: _horizontalBodyScrollController, + thumbVisibility: false, + trackVisibility: false, + notificationPredicate: (notif) => notif.depth == 1, + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + controller: _horizontalBodyScrollController, + child: Container( + color: ColorsManager.whiteColors, + child: SizedBox( + width: widget.size.width, + child: widget.isEmpty + ? _buildEmptyState() + : Column( + children: List.generate(widget.data.length, + (rowIndex) { + final row = widget.data[rowIndex]; + return Row( + children: [ + if (widget.withCheckBox) + _buildRowCheckbox(rowIndex, + widget.size.height * 0.08), + ...row.asMap().entries.map((entry) { + return _buildTableCell( + entry.value.toString(), + widget.size.height * 0.08, + rowIndex: rowIndex, + columnIndex: entry.key, + ); + }).toList(), + ], + ); + }), + ), + ), + ), + ), + ), + ), + ), + ), + ], ), ); } @@ -196,11 +211,39 @@ class _DynamicTableState extends State { ), child: Checkbox( value: _selectAll, - onChanged: widget.withSelectAll && widget.data.isNotEmpty ? _toggleSelectAll : null, + onChanged: widget.withSelectAll && widget.data.isNotEmpty + ? _toggleSelectAll + : null, ), ); } + Widget _buildEmptyState() => Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Column( + children: [ + SvgPicture.asset(Assets.emptyTable), + const SizedBox(height: 15), + Text( + widget.tableName == 'AccessManagement' + ? 'No Password ' + : 'No Devices', + style: Theme.of(context) + .textTheme + .bodySmall! + .copyWith(color: ColorsManager.grayColor), + ) + ], + ), + ], + ), + ], + ); Widget _buildRowCheckbox(int index, double size) { return Container( width: 50, @@ -238,7 +281,9 @@ class _DynamicTableState extends State { constraints: const BoxConstraints.expand(height: 40), alignment: Alignment.centerLeft, child: Padding( - padding: EdgeInsets.symmetric(horizontal: index == widget.headers.length - 1 ? 12 : 8.0, vertical: 4), + padding: EdgeInsets.symmetric( + horizontal: index == widget.headers.length - 1 ? 12 : 8.0, + vertical: 4), child: Text( title, style: context.textTheme.titleSmall!.copyWith( @@ -253,13 +298,28 @@ class _DynamicTableState extends State { ); } - Widget _buildTableCell(String content, double size) { + Widget _buildTableCell( + String content, + double size, { + required int rowIndex, + required int columnIndex, + }) { bool isBatteryLevel = content.endsWith('%'); double? batteryLevel; if (isBatteryLevel) { batteryLevel = double.tryParse(content.replaceAll('%', '').trim()); } + bool isSettingsColumn = widget.headers[columnIndex] == 'Settings'; + + if (isSettingsColumn) { + return buildSettingsIcon( + width: 120, + height: 60, + iconSize: 40, + onTap: () => widget.onSettingsPressed?.call(rowIndex), + ); + } Color? statusColor; switch (content) { @@ -311,4 +371,64 @@ class _DynamicTableState extends State { ), ); } + + Widget buildSettingsIcon( + {double width = 120, + double height = 60, + double iconSize = 40, + VoidCallback? onTap}) { + return Column( + children: [ + Container( + padding: const EdgeInsets.only(top: 10, bottom: 15, left: 10), + margin: const EdgeInsets.only(right: 15), + decoration: const BoxDecoration( + color: ColorsManager.whiteColors, + border: Border( + bottom: BorderSide( + color: ColorsManager.boxDivider, + width: 1.0, + ), + ), + ), + width: width, + child: Padding( + padding: const EdgeInsets.only( + right: 16.0, + left: 17.0, + ), + child: Container( + width: 50, + decoration: BoxDecoration( + color: const Color(0xFFF7F8FA), + borderRadius: BorderRadius.circular(height / 2), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.17), + blurRadius: 14, + offset: const Offset(0, 4), + ), + ], + ), + child: InkWell( + onTap: onTap, + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Center( + child: SvgPicture.asset( + Assets.settings, // ضع المسار الصحيح هنا + width: 40, + height: 22, + color: ColorsManager + .primaryColor, // نفس لون الأيقونة في الصورة + ), + ), + ), + ), + ), + ), + ), + ], + ); + } } diff --git a/lib/pages/common/text_field/custom_web_textfield.dart b/lib/pages/common/text_field/custom_web_textfield.dart index 630e334b..c2daae0c 100644 --- a/lib/pages/common/text_field/custom_web_textfield.dart +++ b/lib/pages/common/text_field/custom_web_textfield.dart @@ -78,7 +78,7 @@ class CustomWebTextField extends StatelessWidget { controller: controller, style: const TextStyle(color: Colors.black), decoration: textBoxDecoration()!.copyWith( - errorStyle: const TextStyle(height: 0), + errorStyle: const TextStyle(height: 0.01), hintStyle: context.textTheme.titleSmall! .copyWith(color: Colors.grey, fontSize: 12), hintText: hintText ?? 'Please enter'), diff --git a/lib/pages/device_managment/ac/bloc/ac_bloc.dart b/lib/pages/device_managment/ac/bloc/ac_bloc.dart index 501d29d8..af5a7b0a 100644 --- a/lib/pages/device_managment/ac/bloc/ac_bloc.dart +++ b/lib/pages/device_managment/ac/bloc/ac_bloc.dart @@ -1,21 +1,27 @@ import 'dart:async'; -import 'package:dio/dio.dart'; + import 'package:firebase_database/firebase_database.dart'; -import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart'; import 'package:syncrow_web/pages/device_managment/ac/model/ac_model.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; class AcBloc extends Bloc { late AcStatusModel deviceStatus; final String deviceId; - Timer? _timer; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; Timer? _countdownTimer; - AcBloc({required this.deviceId}) : super(AcsInitialState()) { + AcBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(AcsInitialState()) { on(_onFetchAcStatus); on(_onFetchAcBatchStatus); on(_onAcControl); @@ -34,14 +40,14 @@ class AcBloc extends Bloc { int scheduledMinutes = 0; FutureOr _onFetchAcStatus( - AcFetchDeviceStatusEvent event, Emitter emit) async { + AcFetchDeviceStatusEvent event, + Emitter emit, + ) async { emit(AcsLoadingState()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = AcStatusModel.fromJson(event.deviceId, status.status); if (deviceStatus.countdown1 != 0) { - // Convert API value to minutes final totalMinutes = deviceStatus.countdown1 * 6; scheduledHours = totalMinutes ~/ 60; scheduledMinutes = totalMinutes % 60; @@ -62,30 +68,24 @@ class AcBloc extends Bloc { } } - _listenToChanges(deviceId) { + void _listenToChanges(deviceId) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) async { if (event.snapshot.value == null) return; - if (_timer != null) { - await Future.delayed(const Duration(seconds: 1)); - } Map usersMap = event.snapshot.value as Map; List statusList = []; usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); + statusList.add(Status(code: element['code'], value: element['value'])); }); - deviceStatus = - AcStatusModel.fromJson(usersMap['productUuid'], statusList); + deviceStatus = AcStatusModel.fromJson(usersMap['productUuid'], statusList); if (!isClosed) { add(AcStatusUpdated(deviceStatus)); } @@ -93,146 +93,44 @@ class AcBloc extends Bloc { } catch (_) {} } - void _onAcStatusUpdated(AcStatusUpdated event, Emitter emit) { + void _onAcStatusUpdated( + AcStatusUpdated event, + Emitter emit, + ) { deviceStatus = event.deviceStatus; emit(ACStatusLoaded(status: deviceStatus)); } FutureOr _onAcControl( - AcControlEvent event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value, emit); - + AcControlEvent event, + Emitter emit, + ) async { + emit(AcsLoadingState()); + _updateDeviceFunctionFromCode(event.code, event.value); emit(ACStatusLoaded(status: deviceStatus)); - await _runDebounce( - isBatch: false, - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - ); - } + try { + final success = await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); - Future _runDebounce({ - required dynamic deviceId, - required String code, - required dynamic value, - required dynamic oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - if (e is DioException && e.response != null) { - debugPrint('Error response: ${e.response?.data}'); - } - _revertValueAndEmit(id, code, oldValue, emit); + if (!success) { + emit(const AcsFailedState(error: 'Failed to control device')); } - }); - } - - void _revertValueAndEmit( - String deviceId, String code, dynamic oldValue, Emitter emit) { - _updateLocalValue(code, oldValue, emit); - emit(ACStatusLoaded(status: deviceStatus)); - } - - void _updateLocalValue(String code, dynamic value, Emitter emit) { - switch (code) { - case 'switch': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(acSwitch: value); - } - break; - case 'temp_set': - if (value is int) { - deviceStatus = deviceStatus.copyWith(tempSet: value); - } - break; - case 'mode': - if (value is String) { - deviceStatus = deviceStatus.copyWith( - modeString: value, - ); - } - break; - case 'level': - if (value is String) { - deviceStatus = deviceStatus.copyWith( - fanSpeedsString: value, - ); - } - break; - case 'child_lock': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(childLock: value); - } - - case 'countdown_time': - if (value is int) { - deviceStatus = deviceStatus.copyWith(countdown1: value); - } - break; - default: - break; - } - emit(ACStatusLoaded(status: deviceStatus)); - } - - dynamic _getValueByCode(String code) { - switch (code) { - case 'switch': - return deviceStatus.acSwitch; - case 'temp_set': - return deviceStatus.tempSet; - case 'mode': - return deviceStatus.modeString; - case 'level': - return deviceStatus.fanSpeedsString; - case 'child_lock': - return deviceStatus.childLock; - case 'countdown_time': - return deviceStatus.countdown1; - default: - return null; + } catch (e) { + emit(AcsFailedState(error: e.toString())); } } FutureOr _onFetchAcBatchStatus( - AcFetchBatchStatusEvent event, Emitter emit) async { + AcFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(AcsLoadingState()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); - deviceStatus = - AcStatusModel.fromJson(event.devicesIds.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = AcStatusModel.fromJson(event.devicesIds.first, status.status); emit(ACStatusLoaded(status: deviceStatus)); } catch (e) { emit(AcsFailedState(error: e.toString())); @@ -240,25 +138,32 @@ class AcBloc extends Bloc { } FutureOr _onAcBatchControl( - AcBatchControlEvent event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value, emit); - + AcBatchControlEvent event, + Emitter emit, + ) async { + emit(AcsLoadingState()); + _updateDeviceFunctionFromCode(event.code, event.value); emit(ACStatusLoaded(status: deviceStatus)); - await _runDebounce( - isBatch: true, - deviceId: event.devicesIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - ); + try { + final success = await batchControlDevicesService.batchControlDevices( + uuids: event.devicesIds, + code: event.code, + value: event.value, + ); + + if (!success) { + emit(const AcsFailedState(error: 'Failed to control devices')); + } + } catch (e) { + emit(AcsFailedState(error: e.toString())); + } } - FutureOr _onFactoryReset( - AcFactoryResetEvent event, Emitter emit) async { + Future _onFactoryReset( + AcFactoryResetEvent event, + Emitter emit, + ) async { emit(AcsLoadingState()); try { final response = await DevicesManagementApi().factoryReset( @@ -275,9 +180,11 @@ class AcBloc extends Bloc { } } - void _onClose(OnClose event, Emitter emit) { + void _onClose( + OnClose event, + Emitter emit, + ) { _countdownTimer?.cancel(); - _timer?.cancel(); } void _handleIncreaseTime(IncreaseTimeEvent event, Emitter emit) { @@ -300,7 +207,10 @@ class AcBloc extends Bloc { )); } - void _handleDecreaseTime(DecreaseTimeEvent event, Emitter emit) { + void _handleDecreaseTime( + DecreaseTimeEvent event, + Emitter emit, + ) { if (state is! ACStatusLoaded) return; final currentState = state as ACStatusLoaded; int totalMinutes = (scheduledHours * 60) + scheduledMinutes; @@ -315,7 +225,9 @@ class AcBloc extends Bloc { } Future _handleToggleTimer( - ToggleScheduleEvent event, Emitter emit) async { + ToggleScheduleEvent event, + Emitter emit, + ) async { if (state is! ACStatusLoaded) return; final currentState = state as ACStatusLoaded; @@ -331,37 +243,44 @@ class AcBloc extends Bloc { try { final scaledValue = totalMinutes ~/ 6; - await _runDebounce( - isBatch: false, - deviceId: deviceId, - code: 'countdown_time', - value: scaledValue, - oldValue: scaledValue, - emit: emit, + final success = await controlDeviceService.controlDevice( + deviceUuid: deviceId, + status: Status(code: 'countdown_time', value: scaledValue), ); - _startCountdownTimer(emit); - emit(currentState.copyWith(isTimerActive: timerActive)); + + if (success) { + _startCountdownTimer(emit); + emit(currentState.copyWith(isTimerActive: timerActive)); + } else { + timerActive = false; + emit(const AcsFailedState(error: 'Failed to set timer')); + } } catch (e) { timerActive = false; emit(AcsFailedState(error: e.toString())); } } else { - await _runDebounce( - isBatch: false, - deviceId: deviceId, - code: 'countdown_time', - value: 0, - oldValue: 0, - emit: emit, - ); - _countdownTimer?.cancel(); - scheduledHours = 0; - scheduledMinutes = 0; - emit(currentState.copyWith( - isTimerActive: timerActive, - scheduledHours: 0, - scheduledMinutes: 0, - )); + try { + final success = await controlDeviceService.controlDevice( + deviceUuid: deviceId, + status: Status(code: 'countdown_time', value: 0), + ); + + if (success) { + _countdownTimer?.cancel(); + scheduledHours = 0; + scheduledMinutes = 0; + emit(currentState.copyWith( + isTimerActive: timerActive, + scheduledHours: 0, + scheduledMinutes: 0, + )); + } else { + emit(const AcsFailedState(error: 'Failed to stop timer')); + } + } catch (e) { + emit(AcsFailedState(error: e.toString())); + } } } @@ -385,7 +304,10 @@ class AcBloc extends Bloc { }); } - void _handleUpdateTimer(UpdateTimerEvent event, Emitter emit) { + void _handleUpdateTimer( + UpdateTimerEvent event, + Emitter emit, + ) { if (state is ACStatusLoaded) { final currentState = state as ACStatusLoaded; emit(currentState.copyWith( @@ -400,7 +322,6 @@ class AcBloc extends Bloc { ApiCountdownValueEvent event, Emitter emit) { if (state is ACStatusLoaded) { final totalMinutes = event.apiValue * 6; - final scheduledHours = totalMinutes ~/ 60; scheduledMinutes = totalMinutes % 60; _startCountdownTimer( emit, @@ -409,6 +330,43 @@ class AcBloc extends Bloc { } } + void _updateDeviceFunctionFromCode(String code, dynamic value) { + switch (code) { + case 'switch': + if (value is bool) { + deviceStatus = deviceStatus.copyWith(acSwitch: value); + } + break; + case 'temp_set': + if (value is int) { + deviceStatus = deviceStatus.copyWith(tempSet: value); + } + break; + case 'mode': + if (value is String) { + deviceStatus = deviceStatus.copyWith(modeString: value); + } + break; + case 'level': + if (value is String) { + deviceStatus = deviceStatus.copyWith(fanSpeedsString: value); + } + break; + case 'child_lock': + if (value is bool) { + deviceStatus = deviceStatus.copyWith(childLock: value); + } + break; + case 'countdown_time': + if (value is int) { + deviceStatus = deviceStatus.copyWith(countdown1: value); + } + break; + default: + break; + } + } + @override Future close() { add(OnClose()); diff --git a/lib/pages/device_managment/ac/factories/ac_bloc_factory.dart b/lib/pages/device_managment/ac/factories/ac_bloc_factory.dart new file mode 100644 index 00000000..9e5f4c1c --- /dev/null +++ b/lib/pages/device_managment/ac/factories/ac_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; + +abstract final class AcBlocFactory { + const AcBlocFactory._(); + + static AcBloc create({ + required String deviceId, + }) { + return AcBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/ac/view/ac_device_batch_control.dart b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart index 3005c1c5..aad0669b 100644 --- a/lib/pages/device_managment/ac/view/ac_device_batch_control.dart +++ b/lib/pages/device_managment/ac/view/ac_device_batch_control.dart @@ -3,12 +3,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart'; +import 'package:syncrow_web/pages/device_managment/ac/factories/ac_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_ac_mode.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_current_temp.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/batch_control_list/batch_fan_speed.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -26,8 +26,9 @@ class AcDeviceBatchControlView extends StatelessWidget with HelperResponsiveLayo final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => - AcBloc(deviceId: devicesIds.first)..add(AcFetchBatchStatusEvent(devicesIds)), + create: (context) => AcBlocFactory.create( + deviceId: devicesIds.first, + )..add(AcFetchBatchStatusEvent(devicesIds)), child: BlocBuilder( builder: (context, state) { if (state is ACStatusLoaded) { diff --git a/lib/pages/device_managment/ac/view/ac_device_control.dart b/lib/pages/device_managment/ac/view/ac_device_control.dart index 8c33c853..a882e6d5 100644 --- a/lib/pages/device_managment/ac/view/ac_device_control.dart +++ b/lib/pages/device_managment/ac/view/ac_device_control.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_event.dart'; import 'package:syncrow_web/pages/device_managment/ac/bloc/ac_state.dart'; +import 'package:syncrow_web/pages/device_managment/ac/factories/ac_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/control_list/ac_mode.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/control_list/current_temp.dart'; import 'package:syncrow_web/pages/device_managment/ac/view/control_list/fan_speed.dart'; @@ -24,8 +25,9 @@ class AcDeviceControlsView extends StatelessWidget with HelperResponsiveLayout { final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => AcBloc(deviceId: device.uuid!) - ..add(AcFetchDeviceStatusEvent(device.uuid!)), + create: (context) => AcBlocFactory.create( + deviceId: device.uuid!, + )..add(AcFetchDeviceStatusEvent(device.uuid!)), child: BlocBuilder( builder: (context, state) { final acBloc = BlocProvider.of(context); diff --git a/lib/pages/device_managment/all_devices/models/devices_model.dart b/lib/pages/device_managment/all_devices/models/devices_model.dart index 485cb0ec..808a683f 100644 --- a/lib/pages/device_managment/all_devices/models/devices_model.dart +++ b/lib/pages/device_managment/all_devices/models/devices_model.dart @@ -12,6 +12,8 @@ import 'package:syncrow_web/pages/routines/models/gang_switches/one_gang_switch/ import 'package:syncrow_web/pages/routines/models/gang_switches/three_gang_switch/three_gang_switch.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/two_gang_switch/two_gang_switch.dart'; import 'package:syncrow_web/pages/routines/models/gateway.dart'; +import 'package:syncrow_web/pages/routines/models/pc/energy_clamp_functions.dart'; +import 'package:syncrow_web/pages/routines/models/water_heater/water_heater_functions.dart'; import 'package:syncrow_web/pages/routines/models/wps/wps_functions.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_helper.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -247,6 +249,8 @@ SOS tempIcon = Assets.waterLeakNormal; } else if (type == DeviceType.NCPS) { tempIcon = Assets.sensors; + } else if (type == DeviceType.PC) { + tempIcon = Assets.powerClamp; } else { tempIcon = Assets.logoHorizontal; } @@ -358,7 +362,10 @@ SOS case 'NCPS': return [ FlushPresenceDelayFunction( - deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF',), + deviceId: uuid ?? '', + deviceName: name ?? '', + type: 'IF', + ), FlushIlluminanceFunction( deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), @@ -378,6 +385,70 @@ SOS FlushTriggerLevelFunction( deviceId: uuid ?? '', deviceName: name ?? '', type: 'THEN'), ]; + case 'WH': + return [ + WHRestartStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + WHSwitchFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'), + TimerConfirmTimeFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'BOTH'), + BacklightFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + ]; + case 'PC': + return [ + TotalEnergyConsumedStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + TotalActivePowerConsumedStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + VoltagePhaseSequenceDetectionFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + TotalCurrentStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + FrequencyStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + + // Phase A + EnergyConsumedAStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + ActivePowerAStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + VoltageAStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + PowerFactorAStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + CurrentAStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + + // Phase B + EnergyConsumedBStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + ActivePowerBStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + VoltageBStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + CurrentBStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + PowerFactorBStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + + // Phase C + EnergyConsumedCStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + ActivePowerCStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + VoltageCStatusFunction( + deviceId: uuid ?? '', deviceName: name ?? '', type: 'IF'), + CurrentCStatusFunction( + deviceId: uuid ?? '', + deviceName: name ?? '', + type: 'IF'), + PowerFactorCStatusFunction( + deviceId: uuid ?? '', + deviceName: name ?? '', + type: 'IF'), + ]; default: return []; @@ -511,5 +582,6 @@ SOS "GD": DeviceType.GarageDoor, "WL": DeviceType.WaterLeak, "NCPS": DeviceType.NCPS, + "PC": DeviceType.PC, }; } diff --git a/lib/pages/device_managment/all_devices/view/device_managment_page.dart b/lib/pages/device_managment/all_devices/view/device_managment_page.dart index fd3a2574..2379c22d 100644 --- a/lib/pages/device_managment/all_devices/view/device_managment_page.dart +++ b/lib/pages/device_managment/all_devices/view/device_managment_page.dart @@ -40,17 +40,18 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { style: TextButton.styleFrom( backgroundColor: null, ), - onPressed: () { - BlocProvider.of(context) - .add(const ResetSelectedEvent()); + onPressed: !state.routineTab + ? null + : () { + BlocProvider.of(context) + .add(const ResetSelectedEvent()); - context - .read() - .add(const TriggerSwitchTabsEvent(isRoutineTab: false)); - context - .read() - .add(FetchDevices(context)); - }, + context.read().add( + const TriggerSwitchTabsEvent(isRoutineTab: false)); + context + .read() + .add(FetchDevices(context)); + }, child: Text( 'Devices', style: context.textTheme.titleMedium?.copyWith( @@ -66,14 +67,15 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { style: TextButton.styleFrom( backgroundColor: null, ), - onPressed: () { - BlocProvider.of(context) - .add(const ResetSelectedEvent()); + onPressed: state.routineTab + ? null + : () { + BlocProvider.of(context) + .add(const ResetSelectedEvent()); - context - .read() - .add(const TriggerSwitchTabsEvent(isRoutineTab: true)); - }, + context.read().add( + const TriggerSwitchTabsEvent(isRoutineTab: true)); + }, child: Text( 'Routines', style: context.textTheme.titleMedium?.copyWith( @@ -95,7 +97,7 @@ class DeviceManagementPage extends StatelessWidget with HelperResponsiveLayout { return const RoutinesView(); } if (state.createRoutineView) { - return CreateNewRoutineView(); + return const CreateNewRoutineView(); } return BlocBuilder( diff --git a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart index a3c975c1..f4baad0c 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_managment_body.dart @@ -6,9 +6,11 @@ import 'package:syncrow_web/pages/common/filter/filter_widget.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/widgets/device_search_filters.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/device_settings_panel.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_batch_control_dialog.dart'; import 'package:syncrow_web/pages/device_managment/shared/device_control_dialog.dart'; import 'package:syncrow_web/pages/space_tree/view/space_tree_view.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/format_date_time.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; import 'package:syncrow_web/utils/style.dart'; @@ -58,7 +60,8 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { 'Low Battery ($lowBatteryCount)', ]; - final buttonLabel = (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; + final buttonLabel = + (selectedDevices.length > 1) ? 'Batch Control' : 'Control'; return Row( children: [ @@ -105,18 +108,23 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { if (selectedDevices.length == 1) { showDialog( context: context, - builder: (context) => DeviceControlDialog( + builder: (context) => + DeviceControlDialog( device: selectedDevices.first, ), ); - } else if (selectedDevices.length > 1) { - final productTypes = selectedDevices - .map((device) => device.productType) - .toSet(); + } else if (selectedDevices.length > + 1) { + final productTypes = + selectedDevices + .map((device) => + device.productType) + .toSet(); if (productTypes.length == 1) { showDialog( context: context, - builder: (context) => DeviceBatchControlDialog( + builder: (context) => + DeviceBatchControlDialog( devices: selectedDevices, ), ); @@ -130,7 +138,9 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { textAlign: TextAlign.center, style: TextStyle( fontSize: 12, - color: isControlButtonEnabled ? Colors.white : Colors.grey, + color: isControlButtonEnabled + ? Colors.white + : Colors.grey, ), ), ), @@ -166,29 +176,40 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { 'Installation Date and Time', 'Status', 'Last Offline Date and Time', + 'Settings' ], data: devicesToShow.map((device) { final combinedSpaceNames = device.spaces != null - ? device.spaces!.map((space) => space.spaceName).join(' > ') + + ? device.spaces! + .map((space) => space.spaceName) + .join(' > ') + (device.community != null ? ' > ${device.community!.name}' : '') - : (device.community != null ? device.community!.name : ''); + : (device.community != null + ? device.community!.name + : ''); return [ device.name ?? '', device.productName ?? '', device.uuid ?? '', - (device.spaces != null && device.spaces!.isNotEmpty) + (device.spaces != null && + device.spaces!.isNotEmpty) ? device.spaces![0].spaceName : '', combinedSpaceNames, - device.batteryLevel != null ? '${device.batteryLevel}%' : '-', - formatDateTime(DateTime.fromMillisecondsSinceEpoch( - (device.createTime ?? 0) * 1000)), + device.batteryLevel != null + ? '${device.batteryLevel}%' + : '-', + formatDateTime( + DateTime.fromMillisecondsSinceEpoch( + (device.createTime ?? 0) * 1000)), device.online == true ? 'Online' : 'Offline', - formatDateTime(DateTime.fromMillisecondsSinceEpoch( - (device.updateTime ?? 0) * 1000)), + formatDateTime( + DateTime.fromMillisecondsSinceEpoch( + (device.updateTime ?? 0) * 1000)), + 'Settings', ]; }).toList(), onSelectionChanged: (selectedRows) { @@ -202,6 +223,10 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { .map((device) => device.uuid!) .toList(), isEmpty: devicesToShow.isEmpty, + onSettingsPressed: (rowIndex) { + final device = devicesToShow[rowIndex]; + showDeviceSettingsSidebar(context, device); + }, ), ), ) @@ -213,4 +238,37 @@ class DeviceManagementBody extends StatelessWidget with HelperResponsiveLayout { }, ); } + + void showDeviceSettingsSidebar(BuildContext context, AllDevicesModel device) { + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: "Device Settings", + transitionDuration: const Duration(milliseconds: 300), + pageBuilder: (context, anim1, anim2) { + return Align( + alignment: Alignment.centerRight, + child: Material( + child: Container( + width: MediaQuery.of(context).size.width * 0.3, + color: ColorsManager.whiteColors, + child: DeviceSettingsPanel( + device: device, + onClose: () => Navigator.of(context).pop(), + ), + ), + ), + ); + }, + transitionBuilder: (context, anim1, anim2, child) { + return SlideTransition( + position: Tween( + begin: const Offset(1, 0), + end: Offset.zero, + ).animate(anim1), + child: child, + ); + }, + ); + } } diff --git a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart index 18d72fc9..6440d18f 100644 --- a/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart +++ b/lib/pages/device_managment/all_devices/widgets/device_search_filters.dart @@ -34,7 +34,8 @@ class _DeviceSearchFiltersState extends State runSpacing: 10, children: [ _buildSearchField("Space Name", _unitNameController, 200), - _buildSearchField("Device Name / Product Name", _productNameController, 300), + _buildSearchField( + "Device Name / Product Name", _productNameController, 300), _buildSearchResetButtons(), ], ); @@ -74,9 +75,7 @@ class _DeviceSearchFiltersState extends State onReset: () { _unitNameController.clear(); _productNameController.clear(); - context.read() - ..add(ResetFilters()) - ..add(FetchDevices(context)); + context.read().add(ResetFilters()); }, ); } diff --git a/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart b/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart index 4e8d5a8b..42387e57 100644 --- a/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart +++ b/lib/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart @@ -1,5 +1,3 @@ -import 'dart:async'; - import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; @@ -7,14 +5,21 @@ import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_e import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/help_description.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; class CeilingSensorBloc extends Bloc { final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; late CeilingSensorModel deviceStatus; - Timer? _timer; - CeilingSensorBloc({required this.deviceId}) : super(CeilingInitialState()) { + CeilingSensorBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(CeilingInitialState()) { on(_fetchCeilingSensorStatus); on(_fetchCeilingSensorBatchControl); on(_changeValue); @@ -26,35 +31,34 @@ class CeilingSensorBloc extends Bloc { on(_onStatusUpdated); } - void _fetchCeilingSensorStatus( - CeilingInitialEvent event, Emitter emit) async { + Future _fetchCeilingSensorStatus( + CeilingInitialEvent event, + Emitter emit, + ) async { emit(CeilingLoadingInitialState()); try { - var response = - await DevicesManagementApi().getDeviceStatus(event.deviceId); + final response = await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = CeilingSensorModel.fromJson(response.status); emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); _listenToChanges(event.deviceId); } catch (e) { emit(CeilingFailedState(error: e.toString())); - return; } } - _listenToChanges(deviceId) { + void _listenToChanges(String deviceId) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; + if (event.snapshot.value == null) return; + + final usersMap = event.snapshot.value as Map; + final statusList = []; - List statusList = []; usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); + statusList.add(Status(code: element['code'], value: element['value'])); }); deviceStatus = CeilingSensorModel.fromJson(statusList); @@ -65,149 +69,127 @@ class CeilingSensorBloc extends Bloc { } catch (_) {} } - void _onStatusUpdated(StatusUpdated event, Emitter emit) { + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { deviceStatus = event.deviceStatus; emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); } - void _changeValue( - CeilingChangeValueEvent event, Emitter emit) async { + Future _changeValue( + CeilingChangeValueEvent event, + Emitter emit, + ) async { emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); - if (event.code == 'sensitivity') { - deviceStatus.sensitivity = event.value; - } else if (event.code == 'none_body_time') { - deviceStatus.noBodyTime = event.value; - } else if (event.code == 'moving_max_dis') { - deviceStatus.maxDistance = event.value; - } else if (event.code == 'scene') { - deviceStatus.spaceType = getSpaceType(event.value); - } + _updateDeviceFunctionFromCode(event.code, event.value); emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: deviceId, - code: event.code, - value: event.value, - emit: emit, - isBatch: false, - ); + + try { + await controlDeviceService.controlDevice( + deviceUuid: deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + emit(CeilingFailedState(error: e.toString())); + } } Future _onBatchControl( - CeilingBatchControlEvent event, Emitter emit) async { + CeilingBatchControlEvent event, + Emitter emit, + ) async { emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); - if (event.code == 'sensitivity') { - deviceStatus.sensitivity = event.value; - } else if (event.code == 'none_body_time') { - deviceStatus.noBodyTime = event.value; - } else if (event.code == 'moving_max_dis') { - deviceStatus.maxDistance = event.value; - } else if (event.code == 'scene') { - deviceStatus.spaceType = getSpaceType(event.value); - } + _updateDeviceFunctionFromCode(event.code, event.value); emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - emit: emit, - isBatch: true, - ); - } - _runDeBouncer({ - required dynamic deviceId, - required String code, - required dynamic value, - required Emitter emit, - required bool isBatch, - }) { - late String id; + try { + final success = await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - add(CeilingInitialEvent(id)); - } - if (response == true && code == 'scene') { - emit(CeilingLoadingInitialState()); - await Future.delayed(const Duration(seconds: 1)); - add(CeilingInitialEvent(id)); - } - } catch (_) { - await Future.delayed(const Duration(milliseconds: 500)); - add(CeilingInitialEvent(id)); + if (!success) { + emit(const CeilingFailedState(error: 'Failed to control devices')); } - }); + } catch (e) { + emit(CeilingFailedState(error: e.toString())); + } } - FutureOr _getDeviceReports(GetCeilingDeviceReportsEvent event, - Emitter emit) async { + void _updateDeviceFunctionFromCode(String code, dynamic value) { + switch (code) { + case 'sensitivity': + deviceStatus.sensitivity = value; + break; + case 'none_body_time': + deviceStatus.noBodyTime = value; + break; + case 'moving_max_dis': + deviceStatus.maxDistance = value; + break; + case 'scene': + deviceStatus.spaceType = getSpaceType(value); + break; + default: + break; + } + } + + Future _getDeviceReports( + GetCeilingDeviceReportsEvent event, + Emitter emit, + ) async { if (event.code.isEmpty) { emit(ShowCeilingDescriptionState(description: reportString)); return; - } else { - emit(CeilingReportsLoadingState()); - // final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch; - // final to = DateTime.now().millisecondsSinceEpoch; + } - try { - // await DevicesManagementApi.getDeviceReportsByDate(deviceId, event.code, from.toString(), to.toString()) - await DevicesManagementApi.getDeviceReports(deviceId, event.code) - .then((value) { - emit(CeilingReportsState(deviceReport: value)); - }); - } catch (e) { - emit(CeilingReportsFailedState(error: e.toString())); - return; - } + emit(CeilingReportsLoadingState()); + try { + final value = await DevicesManagementApi.getDeviceReports( + deviceId, + event.code, + ); + emit(CeilingReportsState(deviceReport: value)); + } catch (e) { + emit(CeilingReportsFailedState(error: e.toString())); } } void _showDescription( - ShowCeilingDescriptionEvent event, Emitter emit) { + ShowCeilingDescriptionEvent event, + Emitter emit, + ) { emit(ShowCeilingDescriptionState(description: event.description)); } void _backToGridView( - BackToCeilingGridViewEvent event, Emitter emit) { + BackToCeilingGridViewEvent event, + Emitter emit, + ) { emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); } - FutureOr _fetchCeilingSensorBatchControl( - CeilingFetchDeviceStatusEvent event, - Emitter emit) async { + Future _fetchCeilingSensorBatchControl( + CeilingFetchDeviceStatusEvent event, + Emitter emit, + ) async { emit(CeilingLoadingInitialState()); try { - var response = - await DevicesManagementApi().getBatchStatus(event.devicesIds); + final response = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = CeilingSensorModel.fromJson(response.status); emit(CeilingUpdateState(ceilingSensorModel: deviceStatus)); } catch (e) { emit(CeilingFailedState(error: e.toString())); - return; } } - FutureOr _onFactoryReset( - CeilingFactoryResetEvent event, Emitter emit) async { + Future _onFactoryReset( + CeilingFactoryResetEvent event, + Emitter emit, + ) async { emit(CeilingLoadingNewSate(ceilingSensorModel: deviceStatus)); try { final response = await DevicesManagementApi().factoryReset( diff --git a/lib/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart b/lib/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart new file mode 100644 index 00000000..d371efb1 --- /dev/null +++ b/lib/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; + +abstract final class CeilingSensorBlocFactory { + const CeilingSensorBlocFactory._(); + + static CeilingSensorBloc create({ + required String deviceId, + }) { + return CeilingSensorBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart index cf645b6f..9b5ab360 100644 --- a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart +++ b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_batch_control.dart @@ -4,9 +4,9 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_space_type.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_update_data.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presense_nobody_time.dart'; @@ -23,8 +23,9 @@ class CeilingSensorBatchControlView extends StatelessWidget with HelperResponsiv final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => CeilingSensorBloc(deviceId: devicesIds.first) - ..add(CeilingFetchDeviceStatusEvent(devicesIds)), + create: (context) => CeilingSensorBlocFactory.create( + deviceId: devicesIds.first, + )..add(CeilingFetchDeviceStatusEvent(devicesIds)), child: BlocBuilder( builder: (context, state) { if (state is CeilingLoadingInitialState || state is CeilingReportsLoadingState) { @@ -110,7 +111,6 @@ class CeilingSensorBatchControlView extends StatelessWidget with HelperResponsiv ), ), ), - // FirmwareUpdateWidget(deviceId: devicesIds.first, version: 4), FactoryResetWidget( callFactoryReset: () { context.read().add( diff --git a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart index 36b676e9..f3017a7c 100644 --- a/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart +++ b/lib/pages/device_managment/ceiling_sensor/view/ceiling_sensor_controls.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_bloc.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_event.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/bloc/ceiling_state.dart'; +import 'package:syncrow_web/pages/device_managment/ceiling_sensor/factories/ceiling_sensor_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/ceiling_sensor/model/ceiling_sensor_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_display_data.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_space_type.dart'; @@ -28,8 +29,9 @@ class CeilingSensorControlsView extends StatelessWidget final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => CeilingSensorBloc(deviceId: device.uuid ?? '') - ..add(CeilingInitialEvent(device.uuid ?? '')), + create: (context) => CeilingSensorBlocFactory.create( + deviceId: device.uuid ?? '', + )..add(CeilingInitialEvent(device.uuid ?? '')), child: BlocBuilder( builder: (context, state) { if (state is CeilingLoadingInitialState || diff --git a/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart b/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart index 251d999f..749a7729 100644 --- a/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart +++ b/lib/pages/device_managment/curtain/bloc/curtain_bloc.dart @@ -1,17 +1,25 @@ import 'dart:async'; + import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; class CurtainBloc extends Bloc { late bool deviceStatus; final String deviceId; - Timer? _timer; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - CurtainBloc({required this.deviceId}) : super(CurtainInitial()) { + CurtainBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(CurtainInitial()) { on(_onFetchDeviceStatus); on(_onFetchBatchStatus); on(_onCurtainControl); @@ -20,32 +28,31 @@ class CurtainBloc extends Bloc { on(_onStatusUpdated); } - FutureOr _onFetchDeviceStatus( - CurtainFetchDeviceStatus event, Emitter emit) async { + Future _onFetchDeviceStatus( + CurtainFetchDeviceStatus event, + Emitter emit, + ) async { emit(CurtainStatusLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - _listenToChanges(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + _listenToChanges(event.deviceId, emit); deviceStatus = _checkStatus(status.status[0].value); - emit(CurtainStatusLoaded(deviceStatus)); } catch (e) { emit(CurtainError(e.toString())); } } - void _listenToChanges(String deviceId) { + void _listenToChanges(String deviceId, Emitter emit) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { final data = event.snapshot.value as Map?; if (data == null) return; - List statusList = []; + final statusList = []; if (data['status'] != null) { for (var element in data['status']) { statusList.add( @@ -57,7 +64,7 @@ class CurtainBloc extends Bloc { } } if (statusList.isNotEmpty) { - bool newStatus = _checkStatus(statusList[0].value); + final newStatus = _checkStatus(statusList[0].value); if (newStatus != deviceStatus) { deviceStatus = newStatus; if (!isClosed) { @@ -71,76 +78,32 @@ class CurtainBloc extends Bloc { } } - void _onStatusUpdated(StatusUpdated event, Emitter emit) { + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { emit(CurtainStatusLoading()); deviceStatus = event.deviceStatus; emit(CurtainStatusLoaded(deviceStatus)); } - FutureOr _onCurtainControl( - CurtainControl event, Emitter emit) async { - final oldValue = deviceStatus; - + Future _onCurtainControl( + CurtainControl event, + Emitter emit, + ) async { + emit(CurtainStatusLoading()); _updateLocalValue(event.value, emit); - emit(CurtainStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); - } - - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; + try { + final controlValue = event.value ? 'open' : 'close'; + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: controlValue), + ); + } catch (e) { + _updateLocalValue(!event.value, emit); + emit(CurtainControlError(e.toString())); } - - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - final controlValue = value ? 'open' : 'close'; - - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, controlValue); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: controlValue)); - } - - if (!response) { - _revertValueAndEmit(id, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, oldValue, emit); - } - }); - } - - void _revertValueAndEmit( - String deviceId, bool oldValue, Emitter emit) { - _updateLocalValue(oldValue, emit); - emit(CurtainStatusLoaded(deviceStatus)); - emit(const CurtainControlError('Failed to control the device.')); } void _updateLocalValue(bool value, Emitter emit) { @@ -152,41 +115,44 @@ class CurtainBloc extends Bloc { return command.toLowerCase() == 'open'; } - FutureOr _onFetchBatchStatus( - CurtainFetchBatchStatus event, Emitter emit) async { + Future _onFetchBatchStatus( + CurtainFetchBatchStatus event, + Emitter emit, + ) async { emit(CurtainStatusLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); - + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = _checkStatus(status.status[0].value); - emit(CurtainStatusLoaded(deviceStatus)); } catch (e) { emit(CurtainError(e.toString())); } } - FutureOr _onCurtainBatchControl( - CurtainBatchControl event, Emitter emit) async { - final oldValue = deviceStatus; - + Future _onCurtainBatchControl( + CurtainBatchControl event, + Emitter emit, + ) async { + emit(CurtainStatusLoading()); _updateLocalValue(event.value, emit); - emit(CurtainStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.devicesIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); + try { + final controlValue = event.value ? 'open' : 'stop'; + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesIds, + code: event.code, + value: controlValue, + ); + } catch (e) { + _updateLocalValue(!event.value, emit); + emit(CurtainControlError(e.toString())); + } } - FutureOr _onFactoryReset( - CurtainFactoryReset event, Emitter emit) async { + Future _onFactoryReset( + CurtainFactoryReset event, + Emitter emit, + ) async { emit(CurtainStatusLoading()); try { final response = await DevicesManagementApi().factoryReset( diff --git a/lib/pages/device_managment/curtain/factories/curtain_bloc_factory.dart b/lib/pages/device_managment/curtain/factories/curtain_bloc_factory.dart new file mode 100644 index 00000000..f6257b0a --- /dev/null +++ b/lib/pages/device_managment/curtain/factories/curtain_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; + +abstract final class CurtainBlocFactory { + const CurtainBlocFactory._(); + + static CurtainBloc create({ + required String deviceId, + }) { + return CurtainBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart b/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart index 7c873e20..41dcaf9e 100644 --- a/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart +++ b/lib/pages/device_managment/curtain/view/curtain_batch_status_view.dart @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/factories/curtain_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -18,7 +19,7 @@ class CurtainBatchStatusView extends StatelessWidget with HelperResponsiveLayout Widget build(BuildContext context) { return BlocProvider( create: (context) => - CurtainBloc(deviceId: devicesIds.first)..add(CurtainFetchBatchStatus(devicesIds)), + CurtainBlocFactory.create(deviceId: devicesIds.first)..add(CurtainFetchBatchStatus(devicesIds)), child: BlocBuilder( builder: (context, state) { if (state is CurtainStatusLoading) { diff --git a/lib/pages/device_managment/curtain/view/curtain_status_view.dart b/lib/pages/device_managment/curtain/view/curtain_status_view.dart index 2afe49f4..84b0a943 100644 --- a/lib/pages/device_managment/curtain/view/curtain_status_view.dart +++ b/lib/pages/device_managment/curtain/view/curtain_status_view.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/common/curtain_toggle.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_bloc.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_event.dart'; import 'package:syncrow_web/pages/device_managment/curtain/bloc/curtain_state.dart'; +import 'package:syncrow_web/pages/device_managment/curtain/factories/curtain_bloc_factory.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; class CurtainStatusControlsView extends StatelessWidget @@ -15,7 +16,7 @@ class CurtainStatusControlsView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => CurtainBloc(deviceId: deviceId) + create: (context) => CurtainBlocFactory.create(deviceId: deviceId) ..add(CurtainFetchDeviceStatus(deviceId)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart new file mode 100644 index 00000000..c996cf72 --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart @@ -0,0 +1,165 @@ +import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/services/devices_mang_api.dart'; +import 'package:syncrow_web/services/space_mana_api.dart'; +import 'package:syncrow_web/utils/snack_bar.dart'; +part 'setting_bloc_event.dart'; + +class SettingDeviceBloc extends Bloc { + final String deviceId; + SettingDeviceBloc({ + required this.deviceId, + }) : super(const DeviceSettingsInitial()) { + on(_fetchDeviceInfo); + on(_saveName); + on(_changeName); + on(_deleteDevice); + on(_fetchRooms); + on(_onAssignDevice); + } + final TextEditingController nameController = TextEditingController(); + List roomsList = []; + bool isEditingName = false; + + bool _validateInputs() { + final nameError = _fullNameValidator(nameController.text); + if (nameError != null) { + CustomSnackBar.displaySnackBar(nameError); + return true; + } + return false; + } + + String? _fullNameValidator(String? value) { + if (value == null) return 'name is required'; + final withoutExtraSpaces = value.replaceAll(RegExp(r"\s+"), ' ').trim(); + if (withoutExtraSpaces.length < 2 || withoutExtraSpaces.length > 30) { + return 'name must be between 2 and 30 characters long'; + } + if (RegExp(r"/[^ a-zA-Z0-9-\']/").hasMatch(withoutExtraSpaces)) { + return 'Only alphanumeric characters, space, dash and single quote are allowed'; + } + return null; + } + + Future _saveName( + SettingBlocSaveName event, Emitter emit) async { + if (_validateInputs()) return; + try { + emit(DeviceSettingsLoading()); + await DevicesManagementApi.putDeviceName( + deviceId: deviceId, deviceName: nameController.text); + add(DeviceSettingInitialInfo()); + CustomSnackBar.displaySnackBar('Save Successfully'); + emit(DeviceSettingsUpdate(deviceName: nameController.text)); + } catch (e) { + emit(DeviceSettingsError(message: e.toString())); + } + } + + Future _fetchDeviceInfo( + DeviceSettingInitialInfo event, Emitter emit) async { + try { + emit(DeviceSettingsLoading()); + var response = await DevicesManagementApi.getDeviceInfo(deviceId); + DeviceInfoModel deviceInfo = DeviceInfoModel.fromJson(response); + nameController.text = deviceInfo.name; + emit(DeviceSettingsUpdate( + deviceName: nameController.text, + deviceInfo: deviceInfo, + roomsList: roomsList, + )); + } catch (e) { + emit(DeviceSettingsError(message: e.toString())); + } + } + + bool editName = false; + final FocusNode focusNode = FocusNode(); + + void _changeName(ChangeNameEvent event, Emitter emit) { + emit(DeviceSettingsInitial( + deviceName: nameController.text, + deviceId: deviceId, + isEditingName: event.value ?? false, + editingNameValue: event.value?.toString() ?? '', + deviceInfo: state.deviceInfo, + )); + editName = event.value!; + if (editName) { + Future.delayed(const Duration(milliseconds: 500), () { + focusNode.requestFocus(); + }); + } else { + add(const SettingBlocSaveName()); + focusNode.unfocus(); + } + emit(DeviceSettingsUpdate( + deviceName: nameController.text, + deviceInfo: state.deviceInfo, + roomsList: roomsList, + )); + } + + void _deleteDevice( + SettingBlocDeleteDevice event, Emitter emit) async { + try { + emit(DeviceSettingsLoading()); + await DevicesManagementApi.resetDevice(devicesUuid: deviceId); + CustomSnackBar.displaySnackBar('Reset Successfully'); + emit(DeviceSettingsUpdate( + deviceName: nameController.text, + deviceInfo: state.deviceInfo, + roomsList: roomsList, + )); + } catch (e) { + emit(DeviceSettingsError(message: e.toString())); + return; + } + } + + void _onAssignDevice( + SettingBlocAssignRoom event, Emitter emit) async { + try { + emit(DeviceSettingsLoading()); + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + await CommunitySpaceManagementApi.assignDeviceToRoom( + communityId: event.communityUuid, + spaceId: event.spaceUuid, + subSpaceId: event.subSpaceUuid, + deviceId: deviceId, + projectId: projectUuid); + add(DeviceSettingInitialInfo()); + CustomSnackBar.displaySnackBar('Save Successfully'); + emit(DeviceSettingsSaveSelectionSuccess()); + } catch (e) { + emit(DeviceSettingsError(message: e.toString())); + return; + } + } + + void _fetchRooms( + SettingBlocFetchRooms event, Emitter emit) async { + try { + emit(DeviceSettingsLoading()); + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + roomsList = await CommunitySpaceManagementApi.getSubSpaceBySpaceId( + communityId: event.communityUuid, + spaceId: event.spaceUuid, + projectId: projectUuid); + emit(DeviceSettingsUpdate( + deviceName: nameController.text, + deviceInfo: state.deviceInfo, + roomsList: roomsList, + )); + } catch (e) { + emit(DeviceSettingsError(message: e.toString())); + return; + } + } +} diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart new file mode 100644 index 00000000..7fb62ed9 --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_event.dart @@ -0,0 +1,69 @@ +part of 'setting_bloc_bloc.dart'; + +abstract class DeviceSettingEvent extends Equatable { + const DeviceSettingEvent(); + @override + List get props => []; +} + +class SettingBlocSaveDeviceName extends DeviceSettingEvent { + final String deviceName; + final String deviceId; + + const SettingBlocSaveDeviceName( + {required this.deviceName, required this.deviceId}); + + @override + List get props => [deviceName, deviceId]; +} + +class SettingBlocStartEditingName extends DeviceSettingEvent {} + +class SettingBlocCancelEditingName extends DeviceSettingEvent {} + +class SettingBlocChangeEditingNameValue extends DeviceSettingEvent { + final String value; + const SettingBlocChangeEditingNameValue(this.value); + + @override + List get props => [value]; +} + +class SettingBlocFetchRooms extends DeviceSettingEvent { + final String communityUuid; + final String spaceUuid; + + const SettingBlocFetchRooms( + {required this.communityUuid, required this.spaceUuid}); + + @override + List get props => [communityUuid, spaceUuid]; +} + +class SettingBlocSaveName extends DeviceSettingEvent { + const SettingBlocSaveName(); +} + +class DeviceSettingInitialInfo extends DeviceSettingEvent {} + +class ChangeNameEvent extends DeviceSettingEvent { + final bool? value; + const ChangeNameEvent({this.value}); +} + +class SettingBlocDeleteDevice extends DeviceSettingEvent {} + +class SettingBlocAssignRoom extends DeviceSettingEvent { + final String communityUuid; + final String spaceUuid; + final String subSpaceUuid; + + const SettingBlocAssignRoom({ + required this.communityUuid, + required this.spaceUuid, + required this.subSpaceUuid, + }); + + @override + List get props => [spaceUuid, communityUuid, subSpaceUuid]; +} diff --git a/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart b/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart new file mode 100644 index 00000000..55054c9a --- /dev/null +++ b/lib/pages/device_managment/device_setting/bloc/setting_bloc_state.dart @@ -0,0 +1,81 @@ +import 'package:equatable/equatable.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; + +abstract class DeviceSettingsState extends Equatable { + const DeviceSettingsState({this.deviceInfo}); + + final DeviceInfoModel? deviceInfo; + + @override + List get props => [deviceInfo]; +} + +class DeviceSettingsInitial extends DeviceSettingsState { + final String deviceName; + final String deviceId; + final bool isEditingName; + final String editingNameValue; + + const DeviceSettingsInitial({ + this.deviceName = '', + this.deviceId = '', + this.isEditingName = false, + this.editingNameValue = '', + super.deviceInfo, + }); + + DeviceSettingsInitial copyWith({ + String? deviceName, + String? deviceId, + bool? isEditingName, + String? editingNameValue, + }) => + DeviceSettingsInitial( + deviceName: deviceName ?? this.deviceName, + deviceId: deviceId ?? this.deviceId, + isEditingName: isEditingName ?? this.isEditingName, + editingNameValue: editingNameValue ?? this.editingNameValue, + ); + + @override + List get props => + [deviceName, deviceId, isEditingName, editingNameValue]; +} + +class DeviceSettingsLoading extends DeviceSettingsState {} + +class DeviceSettingsUpdate extends DeviceSettingsState { + final String? deviceName; + final List roomsList; + + const DeviceSettingsUpdate({ + this.deviceName, + super.deviceInfo, + this.roomsList = const [], + }); + + @override + List get props => [deviceName, deviceInfo, roomsList]; +} + +class DeviceSettingsError extends DeviceSettingsState { + final String message; + + const DeviceSettingsError({required this.message}); + @override + List get props => [message]; +} + +class DeviceSettingsFetchRooms extends DeviceSettingsState { + final List roomsList; + + const DeviceSettingsFetchRooms({required this.roomsList}); + + @override + List get props => [roomsList]; +} + +class DeviceSettingsSaveSelectionSuccess extends DeviceSettingsState {} + +class ChangeNameState extends DeviceSettingsState {} diff --git a/lib/pages/device_managment/device_setting/device_icon_type_helper.dart b/lib/pages/device_managment/device_setting/device_icon_type_helper.dart new file mode 100644 index 00000000..13f8abfe --- /dev/null +++ b/lib/pages/device_managment/device_setting/device_icon_type_helper.dart @@ -0,0 +1,28 @@ +import 'package:syncrow_web/utils/constants/assets.dart'; + +class DeviceIconTypeHelper { + static const Map _iconMap = { + 'AC': Assets.ac, + 'GW': Assets.gateway, + 'CPS': Assets.sensors, + 'DL': Assets.doorLock, + 'WPS': Assets.sensors, + '3G': Assets.gangSwitch, + '2G': Assets.twoGang, + '1G': Assets.oneGang, + 'CUR': Assets.curtain, + 'WH': Assets.waterHeater, + 'DS': Assets.doorSensor, + '1GT': Assets.oneTouchSwitch, + '2GT': Assets.twoTouchSwitch, + '3GT': Assets.threeTouchSwitch, + 'GD': Assets.garageDoor, + 'WL': Assets.waterLeakNormal, + 'NCPS': Assets.sensors, + }; + + static String getDeviceIconByTypeCode(String? typeCode) { + if (typeCode == null) return Assets.logoHorizontal; + return _iconMap[typeCode] ?? Assets.logoHorizontal; + } +} diff --git a/lib/pages/device_managment/device_setting/device_management_content.dart b/lib/pages/device_managment/device_setting/device_management_content.dart new file mode 100644 index 00000000..a087e5bb --- /dev/null +++ b/lib/pages/device_managment/device_setting/device_management_content.dart @@ -0,0 +1,144 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/bloc/device_mgmt_bloc/device_managment_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class DeviceManagementContent extends StatelessWidget { + const DeviceManagementContent({ + super.key, + required this.device, + required this.subSpaces, + required this.deviceInfo, + }); + + final AllDevicesModel device; + final List subSpaces; + final DeviceInfoModel deviceInfo; + + @override + Widget build(BuildContext context) { + Widget infoRow( + {required String label, + required String value, + Widget? trailing, + required Color? valueColor}) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + label, + style: context.theme.textTheme.bodyMedium!.copyWith( + fontSize: 14, + color: ColorsManager.grayColor, + ), + ), + const SizedBox(width: 15), + Expanded( + child: Text( + value, + textAlign: TextAlign.end, + style: context.theme.textTheme.bodyMedium! + .copyWith(fontSize: 14, color: valueColor), + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: 12), + trailing ?? const SizedBox.shrink(), + ], + ), + ); + } + + return DefaultContainer( + padding: EdgeInsets.zero, + child: Column( + children: [ + const SizedBox(height: 5), + Padding( + padding: const EdgeInsets.all(10.0), + child: InkWell( + onTap: () async { + final selectedSubSpace = await showSubSpaceDialog( + context, + communityUuid: device.community!.uuid!, + spaceUuid: device.spaces!.first.uuid!, + subSpaces: subSpaces, + selected: deviceInfo.subspace.uuid, + ); + if (selectedSubSpace != null) { + Future.delayed(const Duration(milliseconds: 500), () { + context.read().add( + SettingBlocAssignRoom( + communityUuid: device.community!.uuid!, + spaceUuid: device.spaces!.first.uuid!, + subSpaceUuid: selectedSubSpace.id ?? '', + ), + ); + }); + } + }, + child: infoRow( + label: 'Sub-Space:', + value: deviceInfo.subspace.subspaceName, + valueColor: ColorsManager.blackColor, + trailing: SvgPicture.asset( + Assets.arrowDown, + width: 10, + height: 10, + color: ColorsManager.greyColor, + )), + ), + ), + const Divider(color: ColorsManager.dividerColor), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'Virtual Address:', + value: deviceInfo.productUuid, + valueColor: ColorsManager.blackColor, + trailing: InkWell( + onTap: () { + Clipboard.setData( + ClipboardData(text: device.productUuid ?? ''), + ); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Virtual Address copied to clipboard'), + ), + ); + }, + child: const Icon( + Icons.copy, + size: 15, + color: ColorsManager.greyColor, + ), + ), + ), + ), + const Divider(color: ColorsManager.dividerColor), + Padding( + padding: const EdgeInsets.all(10.0), + child: infoRow( + label: 'MAC Address:', + valueColor: ColorsManager.blackColor, + value: deviceInfo.macAddress, + ), + ), + const SizedBox(height: 5), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/device_setting/device_settings_panel.dart b/lib/pages/device_managment/device_setting/device_settings_panel.dart new file mode 100644 index 00000000..48458b3b --- /dev/null +++ b/lib/pages/device_managment/device_setting/device_settings_panel.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/device_icon_type_helper.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/device_management_content.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/remove_device_widget.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/device_info_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_state.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class DeviceSettingsPanel extends StatelessWidget { + final VoidCallback? onClose; + final AllDevicesModel device; + const DeviceSettingsPanel({super.key, this.onClose, required this.device}); + + @override + Widget build(BuildContext context) { + final sectionTitle = context.theme.textTheme.titleMedium!.copyWith( + fontWeight: FontWeight.bold, + color: ColorsManager.grayColor, + ); + return BlocProvider( + create: (context) => SettingDeviceBloc( + deviceId: device.uuid ?? '', + ) + ..add(DeviceSettingInitialInfo()) + ..add(SettingBlocFetchRooms( + communityUuid: device.community!.uuid!, + spaceUuid: device.spaces!.first.uuid!, + )), + child: Builder( + builder: (context) { + return BlocBuilder( + builder: (context, state) { + final _bloc = context.read(); + final iconPath = DeviceIconTypeHelper.getDeviceIconByTypeCode( + device.productType); + final deviceInfo = state is DeviceSettingsUpdate + ? state.deviceInfo ?? DeviceInfoModel.empty() + : DeviceInfoModel.empty(); + final subSpaces = + state is DeviceSettingsUpdate ? state.roomsList ?? [] : []; + return Stack( + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.3, + color: ColorsManager.grey25, + padding: const EdgeInsets.all(10), + child: ListView( + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + IconButton( + icon: SvgPicture.asset(Assets.closeSettingsIcon), + onPressed: + onClose ?? () => Navigator.of(context).pop(), + ), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Device Settings', + style: context.theme.textTheme.titleLarge! + .copyWith( + fontWeight: FontWeight.w700, + color: ColorsManager.vividBlue + .withOpacity(0.7), + fontSize: 24), + ), + ], + ), + const SizedBox(height: 24), + DefaultContainer( + borderRadius: BorderRadius.circular(15), + child: Row( + children: [ + Padding( + padding: const EdgeInsets.only(left: 15), + child: CircleAvatar( + radius: 38, + backgroundColor: + ColorsManager.grayBorder.withOpacity(0.5), + child: CircleAvatar( + backgroundColor: ColorsManager.whiteColors, + radius: 36, + child: SvgPicture.asset( + iconPath, + fit: BoxFit.cover, + ), + ), + ), + ), + const SizedBox(width: 25), + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 15), + Text( + 'Device Name:', + style: context.textTheme.bodyMedium! + .copyWith( + color: ColorsManager.grayColor, + ), + ), + SizedBox( + height: 35, + child: Row( + children: [ + SizedBox( + height: 50, + width: 190, + child: TextFormField( + scrollPadding: EdgeInsets.zero, + maxLength: 30, + style: const TextStyle( + color: ColorsManager.blackColor, + fontSize: 16, + ), + textAlign: TextAlign.start, + focusNode: _bloc.focusNode, + controller: _bloc.nameController, + enabled: _bloc.editName, + onFieldSubmitted: (value) { + _bloc.add(const ChangeNameEvent( + value: false)); + }, + decoration: InputDecoration( + isDense: true, + contentPadding: EdgeInsets.zero, + border: InputBorder.none, + fillColor: Colors.white10, + counterText: '', + ), + ), + ), + Column( + children: [ + SizedBox( + width: 15, + height: 25, + child: Visibility( + visible: + _bloc.editName != true, + replacement: const SizedBox(), + child: InkWell( + onTap: () { + _bloc.add( + const ChangeNameEvent( + value: true)); + }, + child: SvgPicture.asset( + Assets + .editNameIconSettings, + color: ColorsManager + .lightGrayBorderColor, + height: 15, + width: 15, + ), + ), + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ], + ), + ), + const SizedBox(height: 32), + Padding( + padding: const EdgeInsets.only(left: 10), + child: Text('Device Management', style: sectionTitle), + ), + DeviceManagementContent( + device: device, + subSpaces: subSpaces.cast(), + deviceInfo: deviceInfo, + ), + const SizedBox(height: 32), + RemoveDeviceWidget(bloc: _bloc), + ], + ), + ), + if (state is DeviceSettingsLoading) + Positioned.fill( + child: Container( + color: Colors.black.withOpacity(0.1), + child: const Center( + child: CircularProgressIndicator( + color: ColorsManager.primaryColor, + ), + ), + ), + ), + ], + ); + }, + ); + }, + ), + ); + } +} diff --git a/lib/pages/device_managment/device_setting/remove_device_widget.dart b/lib/pages/device_managment/device_setting/remove_device_widget.dart new file mode 100644 index 00000000..e65ee125 --- /dev/null +++ b/lib/pages/device_managment/device_setting/remove_device_widget.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'package:syncrow_web/web_layout/default_container.dart'; + +class RemoveDeviceWidget extends StatelessWidget { + const RemoveDeviceWidget({ + super.key, + required SettingDeviceBloc bloc, + }) : _bloc = bloc; + + final SettingDeviceBloc _bloc; + + @override + Widget build(BuildContext context) { + return SizedBox( + width: double.infinity, + child: InkWell( + onTap: () { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + 'Remove Device', + style: context.textTheme.bodyMedium!.copyWith( + fontWeight: FontWeight.w700, + color: ColorsManager.red, + ), + ), + content: Text( + 'Are you sure you want to remove this device?', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.grayColor, + ), + ), + actions: [ + TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.grayColor, + ), + ), + ), + TextButton( + onPressed: () { + _bloc.add(SettingBlocDeleteDevice()); + Navigator.of(context).pop(); + }, + child: Text( + 'Remove', + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.red, + ), + ), + ), + ], + ); + }, + ); + }, + child: DefaultContainer( + padding: const EdgeInsets.all(25), + child: Center( + child: Text( + 'Remove Device', + style: context.textTheme.bodyMedium!.copyWith( + fontSize: 14, + color: ColorsManager.red, + fontWeight: FontWeight.w700), + ), + ), + ), + ), + ); + } +} diff --git a/lib/pages/device_managment/device_setting/settings_model/device_info_model.dart b/lib/pages/device_managment/device_setting/settings_model/device_info_model.dart new file mode 100644 index 00000000..ce9b6750 --- /dev/null +++ b/lib/pages/device_managment/device_setting/settings_model/device_info_model.dart @@ -0,0 +1,183 @@ +class DeviceInfoModel { + final int activeTime; + final String category; + final String categoryName; + final int createTime; + final String gatewayId; + final String icon; + final String ip; + final String lat; + final String localKey; + final String lon; + final String model; + final String name; + final String nodeId; + final bool online; + final String ownerId; + final String productName; + final bool sub; + final String timeZone; + final int updateTime; + final String uuid; + final String productUuid; + final String productType; + final String permissionType; + final String macAddress; + final Subspace subspace; + + DeviceInfoModel({ + required this.activeTime, + required this.category, + required this.categoryName, + required this.createTime, + required this.gatewayId, + required this.icon, + required this.ip, + required this.lat, + required this.localKey, + required this.lon, + required this.model, + required this.name, + required this.nodeId, + required this.online, + required this.ownerId, + required this.productName, + required this.sub, + required this.timeZone, + required this.updateTime, + required this.uuid, + required this.productUuid, + required this.productType, + required this.permissionType, + required this.macAddress, + required this.subspace, + }); + + factory DeviceInfoModel.fromJson(Map json) { + return DeviceInfoModel( + activeTime: json['activeTime'] as int? ?? 0, + category: json['category'] ?? '', + categoryName: json['categoryName'] as String? ?? '', + createTime: json['createTime'] as int? ?? 0, + gatewayId: json['gatewayId'] as String? ?? '', + icon: json['icon'] as String? ?? '', + ip: json['ip'] as String? ?? '', + lat: json['lat'] as String? ?? '', + localKey: json['localKey'] as String? ?? '', + lon: json['lon'] as String? ?? '', + model: json['model'] as String? ?? '', + name: json['name'] as String? ?? '', + nodeId: json['nodeId'] as String? ?? '', + online: json['online'] as bool? ?? false, + ownerId: json['ownerId'] as String? ?? '', + productName: json['productName'] as String? ?? '', + sub: json['sub'] as bool? ?? false, + timeZone: json['timeZone'] as String? ?? '', + updateTime: json['updateTime'] as int? ?? 0, + uuid: json['uuid'] as String? ?? '', + productUuid: json['productUuid'] as String? ?? '', + productType: json['productType'] as String? ?? '', + permissionType: json['permissionType'] as String? ?? '', + macAddress: json['macAddress'] as String? ?? '', + subspace: + Subspace.fromJson(json['subspace'] as Map? ?? {}), + ); + } + + Map toJson() { + return { + 'activeTime': activeTime, + 'category': category, + 'categoryName': categoryName, + 'createTime': createTime, + 'gatewayId': gatewayId, + 'icon': icon, + 'ip': ip, + 'lat': lat, + 'localKey': localKey, + 'lon': lon, + 'model': model, + 'name': name, + 'nodeId': nodeId, + 'online': online, + 'ownerId': ownerId, + 'productName': productName, + 'sub': sub, + 'timeZone': timeZone, + 'updateTime': updateTime, + 'uuid': uuid, + 'productUuid': productUuid, + 'productType': productType, + 'permissionType': permissionType, + 'macAddress': macAddress, + 'subspace': subspace.toJson(), + }; + } + + static DeviceInfoModel empty() { + return DeviceInfoModel( + activeTime: 0, + category: '', + categoryName: '', + createTime: 0, + gatewayId: '', + icon: '', + ip: '', + lat: '', + localKey: '', + lon: '', + model: '', + name: '', + nodeId: '', + online: false, + ownerId: '', + productName: '', + sub: false, + timeZone: '', + updateTime: 0, + uuid: '', + productUuid: '', + productType: '', + permissionType: '', + macAddress: '', + subspace: Subspace( + uuid: '', + createdAt: '', + updatedAt: '', + subspaceName: '', + ), + ); + } +} + +class Subspace { + final String uuid; + final String createdAt; + final String updatedAt; + final String subspaceName; + + Subspace({ + required this.uuid, + required this.createdAt, + required this.updatedAt, + required this.subspaceName, + }); + + factory Subspace.fromJson(Map json) { + return Subspace( + uuid: json['uuid'] as String? ?? '', + createdAt: json['createdAt'] as String? ?? '', + updatedAt: json['updatedAt'] as String? ?? '', + subspaceName: json['subspaceName'] as String? ?? '', + ); + } + + Map toJson() { + return { + 'uuid': uuid, + 'createdAt': createdAt, + 'updatedAt': updatedAt, + 'subspaceName': subspaceName, + }; + } +} diff --git a/lib/pages/device_managment/device_setting/settings_model/sub_space_model.dart b/lib/pages/device_managment/device_setting/settings_model/sub_space_model.dart new file mode 100644 index 00000000..9d3f4036 --- /dev/null +++ b/lib/pages/device_managment/device_setting/settings_model/sub_space_model.dart @@ -0,0 +1,35 @@ +import 'package:syncrow_web/pages/visitor_password/model/device_model.dart'; + +class SubSpaceModel { + final String? id; + final String? name; + List? devices; + + SubSpaceModel({ + required this.id, + required this.name, + required this.devices, + }); + + Map toJson() { + return { + 'id': id, + 'name': name, + 'devices': devices?.map((device) => device.toJson()).toList(), + }; + } + + factory SubSpaceModel.fromJson(Map json) { + List devices = []; + if (json['devices'] != null) { + for (var device in json['devices']) { + devices.add(DeviceModel.fromJson(device)); + } + } + return SubSpaceModel( + id: json['uuid'] as String? ?? '', + name: json['subspaceName'] as String? ?? '', + devices: devices.isNotEmpty ? devices : null as List?, + ); + } +} diff --git a/lib/pages/device_managment/device_setting/sub_space_dialog.dart b/lib/pages/device_managment/device_setting/sub_space_dialog.dart new file mode 100644 index 00000000..8b5f6850 --- /dev/null +++ b/lib/pages/device_managment/device_setting/sub_space_dialog.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/bloc/setting_bloc_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/subspace_dialog_buttons.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubSpaceDialog extends StatefulWidget { + final List subSpaces; + final String? selected; + + const SubSpaceDialog({ + Key? key, + required this.subSpaces, + this.selected, + }) : super(key: key); + + @override + State createState() => _SubSpaceDialogState(); +} + +class _SubSpaceDialogState extends State { + String? _selectedId; + + @override + void initState() { + super.initState(); + _selectedId = widget.selected; + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: ColorsManager.whiteColors, + insetPadding: const EdgeInsets.symmetric(horizontal: 24, vertical: 60), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(28), + ), + child: Container( + width: MediaQuery.of(context).size.width * 0.35, + padding: const EdgeInsets.fromLTRB(0, 24, 0, 0), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + 'Sub-Space', + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w700, + color: ColorsManager.blueColor, + fontSize: 20), + ), + const Divider(), + const SizedBox(height: 10), + ...widget.subSpaces.map((space) { + return RadioListTile( + value: space.id!, + groupValue: _selectedId, + onChanged: (value) { + setState(() { + _selectedId = value; + }); + }, + activeColor: Color(0xFF2962FF), + title: Text( + space.name ?? 'Unnamed Sub-Space', + style: context.textTheme.bodyMedium?.copyWith( + fontSize: 15, + color: ColorsManager.grayColor, + fontWeight: FontWeight.w400, + ), + ), + controlAffinity: ListTileControlAffinity.trailing, + contentPadding: const EdgeInsets.symmetric(horizontal: 24), + ); + }).toList(), + const SizedBox(height: 12), + const Divider(height: 1, thickness: 1), + SubSpaceDialogButtons(selectedId: _selectedId, widget: widget), + ], + ), + ), + ); + } +} + +Future showSubSpaceDialog( + BuildContext context, { + required List subSpaces, + String? selected, + required String communityUuid, + required String spaceUuid, +}) { + return showDialog( + context: context, + builder: (ctx) => BlocProvider.value( + value: BlocProvider.of(context), + child: SubSpaceDialog( + subSpaces: subSpaces, + selected: selected, + ), + ), + ); +} diff --git a/lib/pages/device_managment/device_setting/subspace_dialog_buttons.dart b/lib/pages/device_managment/device_setting/subspace_dialog_buttons.dart new file mode 100644 index 00000000..d9491c80 --- /dev/null +++ b/lib/pages/device_managment/device_setting/subspace_dialog_buttons.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/sub_space_dialog.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class SubSpaceDialogButtons extends StatelessWidget { + const SubSpaceDialogButtons({ + super.key, + required String? selectedId, + required this.widget, + }) : _selectedId = selectedId; + + final String? _selectedId; + final SubSpaceDialog widget; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 50, + child: Row( + children: [ + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + right: BorderSide( + color: ColorsManager.dividerColor, + width: 0.5, + ), + ), + ), + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + 'Cancel', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.textGray, + fontSize: 18, + fontWeight: FontWeight.w500, + ), + ), + ), + ), + ), + Expanded( + child: Container( + decoration: const BoxDecoration( + border: Border( + left: BorderSide( + color: ColorsManager.dividerColor, + width: 0.5, + ), + ), + ), + child: TextButton( + onPressed: _selectedId == null + ? null + : () { + final selectedModel = widget.subSpaces.firstWhere( + (space) => space.id == _selectedId, + orElse: () => + SubSpaceModel(id: null, name: '', devices: []), + ); + Navigator.of(context) + .pop(selectedModel); + }, + child: Text( + 'Confirm', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.secondaryColor, + fontSize: 14, + fontWeight: FontWeight.w400, + ), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/device_managment/factories/device_bloc_dependencies_factory.dart b/lib/pages/device_managment/factories/device_bloc_dependencies_factory.dart new file mode 100644 index 00000000..1c75c38b --- /dev/null +++ b/lib/pages/device_managment/factories/device_bloc_dependencies_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; + +abstract final class DeviceBlocDependenciesFactory { + const DeviceBlocDependenciesFactory._(); + + static ControlDeviceService createControlDeviceService() { + return DebouncedControlDeviceService( + decoratee: RemoteControlDeviceService(), + ); + } + + static BatchControlDevicesService createBatchControlDevicesService() { + return DebouncedBatchControlDevicesService( + decoratee: RemoteBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/flush_mounted_presence_sensor/factories/flush_mounted_presence_sensor_bloc_factory.dart b/lib/pages/device_managment/flush_mounted_presence_sensor/factories/flush_mounted_presence_sensor_bloc_factory.dart index 49fb517f..e842f36b 100644 --- a/lib/pages/device_managment/flush_mounted_presence_sensor/factories/flush_mounted_presence_sensor_bloc_factory.dart +++ b/lib/pages/device_managment/flush_mounted_presence_sensor/factories/flush_mounted_presence_sensor_bloc_factory.dart @@ -1,6 +1,5 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor/bloc/flush_mounted_presence_sensor_bloc.dart'; -import 'package:syncrow_web/services/batch_control_devices_service.dart'; -import 'package:syncrow_web/services/control_device_service.dart'; abstract final class FlushMountedPresenceSensorBlocFactory { const FlushMountedPresenceSensorBlocFactory._(); @@ -10,12 +9,8 @@ abstract final class FlushMountedPresenceSensorBlocFactory { }) { return FlushMountedPresenceSensorBloc( deviceId: deviceId, - controlDeviceService: DebouncedControlDeviceService( - decoratee: RemoteControlDeviceService(), - ), - batchControlDevicesService: DebouncedBatchControlDevicesService( - decoratee: RemoteBatchControlDevicesService(), - ), + controlDeviceService: DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: DeviceBlocDependenciesFactory.createBatchControlDevicesService(), ); } } diff --git a/lib/pages/device_managment/garage_door/bloc/garage_door_bloc.dart b/lib/pages/device_managment/garage_door/bloc/garage_door_bloc.dart index 9083ffbe..28a7e33b 100644 --- a/lib/pages/device_managment/garage_door/bloc/garage_door_bloc.dart +++ b/lib/pages/device_managment/garage_door/bloc/garage_door_bloc.dart @@ -360,7 +360,7 @@ class GarageDoorBloc extends Bloc { delay: deviceStatus.delay + Duration(minutes: 10)); emit(GarageDoorLoadedState(status: deviceStatus)); add(GarageDoorControlEvent( - deviceId: event.deviceId, + deviceId: deviceId, value: deviceStatus.delay.inSeconds, code: 'countdown_1')); } catch (e) { @@ -379,7 +379,7 @@ class GarageDoorBloc extends Bloc { } emit(GarageDoorLoadedState(status: deviceStatus)); add(GarageDoorControlEvent( - deviceId: event.deviceId, + deviceId: deviceId, value: deviceStatus.delay.inSeconds, code: 'countdown_1')); } catch (e) { @@ -396,7 +396,7 @@ class GarageDoorBloc extends Bloc { _updateLocalValue(event.code, event.value); emit(GarageDoorLoadedState(status: deviceStatus)); final success = await _runDeBouncer( - deviceId: event.deviceId, + deviceId: deviceId, code: event.code, value: event.value, oldValue: oldValue, diff --git a/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart index 12aeaa88..c1e976ab 100644 --- a/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart @@ -1,11 +1,13 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; import 'package:firebase_database/firebase_database.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; part 'one_gang_glass_switch_event.dart'; @@ -13,13 +15,16 @@ part 'one_gang_glass_switch_state.dart'; class OneGangGlassSwitchBloc extends Bloc { - OneGangGlassStatusModel deviceStatus; - Timer? _timer; + late OneGangGlassStatusModel deviceStatus; + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - OneGangGlassSwitchBloc({required String deviceId}) - : deviceStatus = OneGangGlassStatusModel( - uuid: deviceId, switch1: false, countDown: 0), - super(OneGangGlassSwitchInitial()) { + OneGangGlassSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(OneGangGlassSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onBatchControl); @@ -28,160 +33,140 @@ class OneGangGlassSwitchBloc on(_onStatusUpdated); } - Future _onFetchDeviceStatus(OneGangGlassSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + OneGangGlassSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(OneGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - _listenToChanges(event.deviceId); - deviceStatus = - OneGangGlassStatusModel.fromJson(event.deviceId, status.status); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + _listenToChanges(event.deviceId, emit); + deviceStatus = OneGangGlassStatusModel.fromJson(event.deviceId, status.status); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(OneGangGlassSwitchError(e.toString())); } } - _listenToChanges(deviceId) { + void _listenToChanges( + String deviceId, + Emitter emit, + ) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; + final data = event.snapshot.value as Map?; + if (data == null) return; - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = OneGangGlassStatusModel.fromJson( - usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); + final statusList = []; + if (data['status'] != null) { + for (var element in data['status']) { + statusList.add( + Status( + code: element['code'].toString(), + value: element['value'].toString(), + ), + ); + } + } + if (statusList.isNotEmpty) { + final newStatus = OneGangGlassStatusModel.fromJson(deviceId, statusList); + if (newStatus != deviceStatus) { + deviceStatus = newStatus; + if (!isClosed) { + add(StatusUpdated(deviceStatus)); + } + } } }); - } catch (_) {} + } catch (e) { + emit(OneGangGlassSwitchError('Failed to listen to changes: $e')); + } } void _onStatusUpdated( - StatusUpdated event, Emitter emit) { + StatusUpdated event, + Emitter emit, + ) { + emit(OneGangGlassSwitchLoading()); deviceStatus = event.deviceStatus; emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); } - Future _onControl(OneGangGlassSwitchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onControl( + OneGangGlassSwitchControl event, + Emitter emit, + ) async { + emit(OneGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); - } - - Future _onFactoryReset(OneGangGlassFactoryResetEvent event, - Emitter emit) async { - emit(OneGangGlassSwitchLoading()); try { - final response = await DevicesManagementApi() - .factoryReset(event.factoryReset, event.deviceId); - if (!response) { - emit(OneGangGlassSwitchError('Failed to reset device')); - } else { - emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); - } + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); } catch (e) { + _updateLocalValue(event.code, !event.value); emit(OneGangGlassSwitchError(e.toString())); } } - Future _onBatchControl(OneGangGlassSwitchBatchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onBatchControl( + OneGangGlassSwitchBatchControl event, + Emitter emit, + ) async { + emit(OneGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(OneGangGlassSwitchError(e.toString())); + } } Future _onFetchBatchStatus( - OneGangGlassSwitchFetchBatchStatusEvent event, - Emitter emit) async { + OneGangGlassSwitchFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(OneGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.deviceIds); - deviceStatus = OneGangGlassStatusModel.fromJson( - event.deviceIds.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); + deviceStatus = + OneGangGlassStatusModel.fromJson(event.deviceIds.first, status.status); emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(OneGangGlassSwitchError(e.toString())); } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); + Future _onFactoryReset( + OneGangGlassFactoryResetEvent event, + Emitter emit, + ) async { + emit(OneGangGlassSwitchLoading()); + try { + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); + if (!response) { + emit(OneGangGlassSwitchError('Failed to reset device')); + } else { + add(OneGangGlassSwitchFetchDeviceEvent(event.deviceId)); } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); - emit(OneGangGlassSwitchStatusLoaded(deviceStatus)); + } catch (e) { + emit(OneGangGlassSwitchError(e.toString())); + } } void _updateLocalValue(String code, bool value) { @@ -189,19 +174,4 @@ class OneGangGlassSwitchBloc deviceStatus = deviceStatus.copyWith(switch1: value); } } - - bool _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.switch1; - default: - return false; - } - } - - @override - Future close() { - _timer?.cancel(); - return super.close(); - } } diff --git a/lib/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart b/lib/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart new file mode 100644 index 00000000..97bcab81 --- /dev/null +++ b/lib/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart'; + +abstract final class OneGangGlassSwitchBlocFactory { + const OneGangGlassSwitchBlocFactory._(); + + static OneGangGlassSwitchBloc create({ + required String deviceId, + }) { + return OneGangGlassSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart index 9b89e876..307e61da 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_batch_control_view.dart @@ -2,9 +2,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -16,7 +16,7 @@ class OneGangGlassSwitchBatchControlView extends StatelessWidget with HelperResp @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => OneGangGlassSwitchBloc(deviceId: deviceIds.first) + create: (context) => OneGangGlassSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(OneGangGlassSwitchFetchBatchStatusEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart index 8914b786..997be513 100644 --- a/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/one_g_glass_switch/view/one_gang_glass_switch_control_view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/bloc/one_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/factories/one_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_g_glass_switch/models/once_gang_glass_status_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -9,13 +10,13 @@ import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_la class OneGangGlassSwitchControlView extends StatelessWidget with HelperResponsiveLayout { final String deviceId; - const OneGangGlassSwitchControlView({required this.deviceId, Key? key}) : super(key: key); + const OneGangGlassSwitchControlView({required this.deviceId, super.key}); @override Widget build(BuildContext context) { return BlocProvider( create: (context) => - OneGangGlassSwitchBloc(deviceId: deviceId)..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)), + OneGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(OneGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { if (state is OneGangGlassSwitchLoading) { diff --git a/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart index c2038330..59eccfe9 100644 --- a/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart +++ b/lib/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart @@ -6,12 +6,21 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_sta import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; -class WallLightSwitchBloc - extends Bloc { - WallLightSwitchBloc({required this.deviceId}) - : super(WallLightSwitchInitial()) { +class WallLightSwitchBloc extends Bloc { + late WallLightStatusModel deviceStatus; + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; + + WallLightSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(WallLightSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onFetchBatchStatus); @@ -20,143 +29,114 @@ class WallLightSwitchBloc on(_onStatusUpdated); } - late WallLightStatusModel deviceStatus; - final String deviceId; - Timer? _timer; - - FutureOr _onFetchDeviceStatus(WallLightSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + WallLightSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(WallLightSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - - deviceStatus = - WallLightStatusModel.fromJson(event.deviceId, status.status); - _listenToChanges(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + _listenToChanges(event.deviceId, emit); + deviceStatus = WallLightStatusModel.fromJson(event.deviceId, status.status); emit(WallLightSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(WallLightSwitchError(e.toString())); } } - _listenToChanges(deviceId) { + void _listenToChanges( + String deviceId, + Emitter emit, + ) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; + final data = event.snapshot.value as Map?; + if (data == null) return; - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = - WallLightStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); + final statusList = []; + if (data['status'] != null) { + for (var element in data['status']) { + statusList.add( + Status( + code: element['code'].toString(), + value: element['value'].toString(), + ), + ); + } + } + if (statusList.isNotEmpty) { + final newStatus = WallLightStatusModel.fromJson(deviceId, statusList); + if (newStatus != deviceStatus) { + deviceStatus = newStatus; + if (!isClosed) { + add(StatusUpdated(deviceStatus)); + } + } } }); - } catch (_) {} + } catch (e) { + emit(WallLightSwitchError('Failed to listen to changes: $e')); + } } void _onStatusUpdated( - StatusUpdated event, Emitter emit) { + StatusUpdated event, + Emitter emit, + ) { + emit(WallLightSwitchLoading()); deviceStatus = event.deviceStatus; emit(WallLightSwitchStatusLoaded(deviceStatus)); } - FutureOr _onControl( - WallLightSwitchControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onControl( + WallLightSwitchControl event, + Emitter emit, + ) async { + emit(WallLightSwitchLoading()); _updateLocalValue(event.code, event.value); - emit(WallLightSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(WallLightSwitchError(e.toString())); + } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); + Future _onBatchControl( + WallLightSwitchBatchControl event, + Emitter emit, + ) async { + emit(WallLightSwitchLoading()); + _updateLocalValue(event.code, event.value); emit(WallLightSwitchStatusLoaded(deviceStatus)); - } - void _updateLocalValue(String code, bool value) { - if (code == 'switch_1') { - deviceStatus = deviceStatus.copyWith(switch1: value); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(WallLightSwitchError(e.toString())); } } - bool _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.switch1; - default: - return false; - } - } - - Future _onFetchBatchStatus(WallLightSwitchFetchBatchEvent event, - Emitter emit) async { + Future _onFetchBatchStatus( + WallLightSwitchFetchBatchEvent event, + Emitter emit, + ) async { emit(WallLightSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = WallLightStatusModel.fromJson(event.devicesIds.first, status.status); emit(WallLightSwitchStatusLoaded(deviceStatus)); @@ -165,32 +145,10 @@ class WallLightSwitchBloc } } - @override - Future close() { - _timer?.cancel(); - return super.close(); - } - - FutureOr _onBatchControl(WallLightSwitchBatchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value); - - emit(WallLightSwitchStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.devicesIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); - } - - FutureOr _onFactoryReset( - WallLightFactoryReset event, Emitter emit) async { + Future _onFactoryReset( + WallLightFactoryReset event, + Emitter emit, + ) async { emit(WallLightSwitchLoading()); try { final response = await DevicesManagementApi().factoryReset( @@ -198,12 +156,18 @@ class WallLightSwitchBloc event.deviceId, ); if (!response) { - emit(WallLightSwitchError('Failed')); + emit(WallLightSwitchError('Failed to reset device')); } else { - emit(WallLightSwitchStatusLoaded(deviceStatus)); + add(WallLightSwitchFetchDeviceEvent(event.deviceId)); } } catch (e) { emit(WallLightSwitchError(e.toString())); } } + + void _updateLocalValue(String code, bool value) { + if (code == 'switch_1') { + deviceStatus = deviceStatus.copyWith(switch1: value); + } + } } diff --git a/lib/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart b/lib/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart new file mode 100644 index 00000000..fbbe13dc --- /dev/null +++ b/lib/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart'; + +abstract final class WallLightSwitchBlocFactory { + const WallLightSwitchBlocFactory._(); + + static WallLightSwitchBloc create({ + required String deviceId, + }) { + return WallLightSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart index 7094b506..7fe57429 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_batch_control.dart @@ -4,9 +4,9 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -18,7 +18,7 @@ class WallLightBatchControlView extends StatelessWidget with HelperResponsiveLay @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => WallLightSwitchBloc(deviceId: deviceIds.first) + create: (context) => WallLightSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(WallLightSwitchFetchBatchEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart index a9e6ebbb..f1861c55 100644 --- a/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/one_gang_switch/view/wall_light_device_control.dart @@ -3,6 +3,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/bloc/wall_light_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/one_gang_switch/factories/wall_light_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/one_gang_switch/models/wall_light_status_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -15,7 +16,7 @@ class WallLightDeviceControl extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => WallLightSwitchBloc(deviceId: deviceId) + create: (context) => WallLightSwitchBlocFactory.create(deviceId: deviceId) ..add(WallLightSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/shared/device_control_dialog.dart b/lib/pages/device_managment/shared/device_control_dialog.dart index 7304dd07..beb3b52c 100644 --- a/lib/pages/device_managment/shared/device_control_dialog.dart +++ b/lib/pages/device_managment/shared/device_control_dialog.dart @@ -97,7 +97,8 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { children: [ _buildInfoRow('Space Name:', device.spaces?.firstOrNull?.spaceName ?? 'N/A'), - _buildInfoRow('Room:', device.subspace?.subspaceName ?? 'N/A'), + _buildInfoRow( + 'Sub space:', device.subspace?.subspaceName ?? 'N/A'), ], ), TableRow( @@ -156,7 +157,7 @@ class DeviceControlDialog extends StatelessWidget with RouteControlsBasedCode { ), ), const SizedBox(width: 10), - Text( + SelectableText( value, style: TextStyle( fontSize: 16, diff --git a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart index 174cd167..766c3163 100644 --- a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart @@ -1,11 +1,14 @@ import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; import 'package:firebase_database/firebase_database.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/models/three_gang_glass_switch.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; part 'three_gang_glass_switch_event.dart'; @@ -13,19 +16,16 @@ part 'three_gang_glass_switch_state.dart'; class ThreeGangGlassSwitchBloc extends Bloc { - ThreeGangGlassStatusModel deviceStatus; - Timer? _timer; + late ThreeGangGlassStatusModel deviceStatus; + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - ThreeGangGlassSwitchBloc({required String deviceId}) - : deviceStatus = ThreeGangGlassStatusModel( - uuid: deviceId, - switch1: false, - countDown1: 0, - switch2: false, - countDown2: 0, - switch3: false, - countDown3: 0), - super(ThreeGangGlassSwitchInitial()) { + ThreeGangGlassSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(ThreeGangGlassSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onBatchControl); @@ -34,188 +34,154 @@ class ThreeGangGlassSwitchBloc on(_onStatusUpdated); } - Future _onFetchDeviceStatus(ThreeGangGlassSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + ThreeGangGlassSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(ThreeGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + _listenToChanges(event.deviceId, emit); deviceStatus = ThreeGangGlassStatusModel.fromJson(event.deviceId, status.status); - _listenToChanges(event.deviceId); emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(ThreeGangGlassSwitchError(e.toString())); } } - _listenToChanges(deviceId) { + void _listenToChanges( + String deviceId, + Emitter emit, + ) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + final stream = ref.onValue; stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; + final data = event.snapshot.value as Map?; + if (data == null) return; - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = ThreeGangGlassStatusModel.fromJson( - usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); + final statusList = []; + if (data['status'] != null) { + for (var element in data['status']) { + statusList.add( + Status( + code: element['code'].toString(), + value: element['value'].toString(), + ), + ); + } + } + if (statusList.isNotEmpty) { + final newStatus = ThreeGangGlassStatusModel.fromJson(deviceId, statusList); + if (newStatus != deviceStatus) { + deviceStatus = newStatus; + if (!isClosed) { + add(StatusUpdated(deviceStatus)); + } + } } }); - } catch (_) {} + } catch (e) { + emit(ThreeGangGlassSwitchError('Failed to listen to changes: $e')); + } } void _onStatusUpdated( - StatusUpdated event, Emitter emit) { + StatusUpdated event, + Emitter emit, + ) { + emit(ThreeGangGlassSwitchLoading()); deviceStatus = event.deviceStatus; emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); } - Future _onControl(ThreeGangGlassSwitchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onControl( + ThreeGangGlassSwitchControl event, + Emitter emit, + ) async { + emit(ThreeGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(ThreeGangGlassSwitchError(e.toString())); + } } - Future _onBatchControl(ThreeGangGlassSwitchBatchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onBatchControl( + ThreeGangGlassSwitchBatchControl event, + Emitter emit, + ) async { + emit(ThreeGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(ThreeGangGlassSwitchBatchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(ThreeGangGlassSwitchError(e.toString())); + } } Future _onFetchBatchStatus( - ThreeGangGlassSwitchFetchBatchStatusEvent event, - Emitter emit) async { + ThreeGangGlassSwitchFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(ThreeGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.deviceIds); - deviceStatus = ThreeGangGlassStatusModel.fromJson( - event.deviceIds.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); + deviceStatus = + ThreeGangGlassStatusModel.fromJson(event.deviceIds.first, status.status); emit(ThreeGangGlassSwitchBatchStatusLoaded(deviceStatus)); } catch (e) { emit(ThreeGangGlassSwitchError(e.toString())); } } - Future _onFactoryReset(ThreeGangGlassFactoryReset event, - Emitter emit) async { + Future _onFactoryReset( + ThreeGangGlassFactoryReset event, + Emitter emit, + ) async { emit(ThreeGangGlassSwitchLoading()); try { - final response = await DevicesManagementApi() - .factoryReset(event.factoryReset, event.deviceId); + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); if (!response) { - emit(ThreeGangGlassSwitchError('Failed')); + emit(ThreeGangGlassSwitchError('Failed to reset device')); } else { - emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); + add(ThreeGangGlassSwitchFetchDeviceEvent(event.deviceId)); } } catch (e) { emit(ThreeGangGlassSwitchError(e.toString())); } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); - emit(ThreeGangGlassSwitchStatusLoaded(deviceStatus)); - } - void _updateLocalValue(String code, bool value) { - if (code == 'switch_1') { - deviceStatus = deviceStatus.copyWith(switch1: value); - } else if (code == 'switch_2') { - deviceStatus = deviceStatus.copyWith(switch2: value); - } else if (code == 'switch_3') { - deviceStatus = deviceStatus.copyWith(switch3: value); - } - } - - bool _getValueByCode(String code) { switch (code) { case 'switch_1': - return deviceStatus.switch1; + deviceStatus = deviceStatus.copyWith(switch1: value); + break; case 'switch_2': - return deviceStatus.switch2; + deviceStatus = deviceStatus.copyWith(switch2: value); + break; case 'switch_3': - return deviceStatus.switch3; - default: - return false; + deviceStatus = deviceStatus.copyWith(switch3: value); + break; } } - - @override - Future close() { - _timer?.cancel(); - return super.close(); - } } diff --git a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart index 82b93fba..991de938 100644 --- a/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart +++ b/lib/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_event.dart @@ -1,7 +1,10 @@ part of 'three_gang_glass_switch_bloc.dart'; @immutable -abstract class ThreeGangGlassSwitchEvent {} +abstract class ThreeGangGlassSwitchEvent extends Equatable { + @override + List get props => []; +} class ThreeGangGlassSwitchFetchDeviceEvent extends ThreeGangGlassSwitchEvent { final String deviceId; @@ -19,6 +22,9 @@ class ThreeGangGlassSwitchControl extends ThreeGangGlassSwitchEvent { required this.code, required this.value, }); + + @override + List get props => [deviceId, code, value]; } class ThreeGangGlassSwitchBatchControl extends ThreeGangGlassSwitchEvent { @@ -31,6 +37,9 @@ class ThreeGangGlassSwitchBatchControl extends ThreeGangGlassSwitchEvent { required this.code, required this.value, }); + + @override + List get props => [deviceIds, code, value]; } class ThreeGangGlassSwitchFetchBatchStatusEvent @@ -38,6 +47,9 @@ class ThreeGangGlassSwitchFetchBatchStatusEvent final List deviceIds; ThreeGangGlassSwitchFetchBatchStatusEvent(this.deviceIds); + + @override + List get props => [deviceIds]; } class ThreeGangGlassFactoryReset extends ThreeGangGlassSwitchEvent { @@ -48,6 +60,9 @@ class ThreeGangGlassFactoryReset extends ThreeGangGlassSwitchEvent { required this.deviceId, required this.factoryReset, }); + + @override + List get props => [deviceId, factoryReset]; } class StatusUpdated extends ThreeGangGlassSwitchEvent { diff --git a/lib/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart b/lib/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart new file mode 100644 index 00000000..9f66773a --- /dev/null +++ b/lib/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart'; + +abstract final class ThreeGangGlassSwitchBlocFactory { + const ThreeGangGlassSwitchBlocFactory._(); + + static ThreeGangGlassSwitchBloc create({ + required String deviceId, + }) { + return ThreeGangGlassSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart index 071d6ca0..93fbe53e 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_batch_control_view.dart @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_ // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/models/three_gang_glass_switch.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -16,7 +17,7 @@ class ThreeGangGlassSwitchBatchControlView extends StatelessWidget with HelperRe @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => ThreeGangGlassSwitchBloc(deviceId: deviceIds.first) + create: (context) => ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(ThreeGangGlassSwitchFetchBatchStatusEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart index 433e5408..21a81df0 100644 --- a/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/three_g_glass_switch/view/three_gang_glass_switch_control_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/bloc/three_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_g_glass_switch/factories/three_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -16,7 +17,7 @@ class ThreeGangGlassSwitchControlView extends StatelessWidget with HelperRespons Widget build(BuildContext context) { return BlocProvider( create: (context) => - ThreeGangGlassSwitchBloc(deviceId: deviceId)..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)), + ThreeGangGlassSwitchBlocFactory.create(deviceId: deviceId)..add(ThreeGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { if (state is ThreeGangGlassSwitchLoading) { diff --git a/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart b/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart index a7a03a7f..bec1314c 100644 --- a/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart +++ b/lib/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart @@ -1,12 +1,14 @@ -// ignore_for_file: invalid_use_of_visible_for_testing_member - import 'dart:async'; -import 'package:bloc/bloc.dart'; +import 'dart:developer'; + import 'package:equatable/equatable.dart'; import 'package:firebase_database/firebase_database.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; part 'living_room_event.dart'; @@ -15,9 +17,14 @@ part 'living_room_state.dart'; class LivingRoomBloc extends Bloc { late LivingRoomStatusModel deviceStatus; final String deviceId; - Timer? _timer; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - LivingRoomBloc({required this.deviceId}) : super(LivingRoomInitial()) { + LivingRoomBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(LivingRoomInitial()) { on(_onFetchDeviceStatus); on(_livingRoomControl); on(_livingRoomBatchControl); @@ -26,156 +33,108 @@ class LivingRoomBloc extends Bloc { on(_onStatusUpdated); } - FutureOr _onFetchDeviceStatus(LivingRoomFetchDeviceStatusEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + LivingRoomFetchDeviceStatusEvent event, + Emitter emit, + ) async { emit(LivingRoomDeviceStatusLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - deviceStatus = - LivingRoomStatusModel.fromJson(event.deviceId, status.status); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); _listenToChanges(deviceId); + deviceStatus = LivingRoomStatusModel.fromJson(event.deviceId, status.status); emit(LivingRoomDeviceStatusLoaded(deviceStatus)); } catch (e) { emit(LivingRoomDeviceManagementError(e.toString())); } } - FutureOr _livingRoomControl( - LivingRoomControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); + void _listenToChanges(String deviceId) { + try { + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + ref.onValue.listen((event) { + final eventsMap = event.snapshot.value as Map; + List statusList = []; + eventsMap['status'].forEach((element) { + statusList.add( + Status(code: element['code'], value: element['value']), + ); + }); + + deviceStatus = LivingRoomStatusModel.fromJson(deviceId, statusList); + add(StatusUpdated(deviceStatus)); + }); + } catch (_) { + log('Error listening to changes'); + } + } + + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { + deviceStatus = event.deviceStatus; + emit(LivingRoomDeviceStatusLoaded(deviceStatus)); + } + + Future _livingRoomControl( + LivingRoomControl event, + Emitter emit, + ) async { + emit(LivingRoomDeviceStatusLoading()); _updateLocalValue(event.code, event.value); - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(LivingRoomDeviceManagementError(e.toString())); + } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required dynamic value, - required dynamic oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, dynamic oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); + Future _livingRoomBatchControl( + LivingRoomBatchControl event, + Emitter emit, + ) async { + emit(LivingRoomDeviceStatusLoading()); + _updateLocalValue(event.code, event.value); emit(LivingRoomDeviceStatusLoaded(deviceStatus)); - } - void _updateLocalValue(String code, dynamic value) { - switch (code) { - case 'switch_1': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(switch1: value); - } - break; - case 'switch_2': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(switch2: value); - } - break; - case 'switch_3': - if (value is bool) { - deviceStatus = deviceStatus.copyWith(switch3: value); - } - break; - default: - break; - } - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); - } - - dynamic _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.switch1; - case 'switch_2': - return deviceStatus.switch2; - case 'switch_3': - return deviceStatus.switch3; - default: - return null; + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.devicesIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(LivingRoomDeviceManagementError(e.toString())); } } - FutureOr _livingRoomFetchBatchControl( - LivingRoomFetchBatchEvent event, Emitter emit) async { + Future _livingRoomFetchBatchControl( + LivingRoomFetchBatchEvent event, + Emitter emit, + ) async { emit(LivingRoomDeviceStatusLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = LivingRoomStatusModel.fromJson(event.devicesIds.first, status.status); - // for (var deviceId in event.devicesIds) { - // _listenToChanges(deviceId); - // } emit(LivingRoomDeviceStatusLoaded(deviceStatus)); } catch (e) { emit(LivingRoomDeviceManagementError(e.toString())); } } - FutureOr _livingRoomBatchControl( - LivingRoomBatchControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value); - - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.devicesIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); - } - - FutureOr _livingRoomFactoryReset( - LivingRoomFactoryResetEvent event, Emitter emit) async { + Future _livingRoomFactoryReset( + LivingRoomFactoryResetEvent event, + Emitter emit, + ) async { emit(LivingRoomDeviceStatusLoading()); try { final response = await DevicesManagementApi().factoryReset( @@ -183,42 +142,28 @@ class LivingRoomBloc extends Bloc { event.uuid, ); if (!response) { - emit(const LivingRoomDeviceManagementError('Failed')); + emit(const LivingRoomDeviceManagementError('Failed to reset device')); } else { - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); + add(LivingRoomFetchDeviceStatusEvent(event.uuid)); } } catch (e) { emit(LivingRoomDeviceManagementError(e.toString())); } } - _listenToChanges(deviceId) { - try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; + void _updateLocalValue(String code, dynamic value) { + if (value is! bool) return; - stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; - - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = - LivingRoomStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); - } - }); - } catch (_) {} - } - - void _onStatusUpdated(StatusUpdated event, Emitter emit) { - deviceStatus = event.deviceStatus; - emit(LivingRoomDeviceStatusLoaded(deviceStatus)); + switch (code) { + case 'switch_1': + deviceStatus = deviceStatus.copyWith(switch1: value); + break; + case 'switch_2': + deviceStatus = deviceStatus.copyWith(switch2: value); + break; + case 'switch_3': + deviceStatus = deviceStatus.copyWith(switch3: value); + break; + } } } diff --git a/lib/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart b/lib/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart new file mode 100644 index 00000000..94c2b72f --- /dev/null +++ b/lib/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; + +abstract final class LivingRoomBlocFactory { + const LivingRoomBlocFactory._(); + + static LivingRoomBloc create({ + required String deviceId, + }) { + return LivingRoomBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart index 97c25287..0b1a2f06 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_batch_controls.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_re import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -17,7 +18,7 @@ class LivingRoomBatchControlsView extends StatelessWidget with HelperResponsiveL Widget build(BuildContext context) { return BlocProvider( create: (context) => - LivingRoomBloc(deviceId: deviceIds.first)..add(LivingRoomFetchBatchEvent(deviceIds)), + LivingRoomBlocFactory.create(deviceId: deviceIds.first)..add(LivingRoomFetchBatchEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { if (state is LivingRoomDeviceStatusLoading) { diff --git a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart index b7f97776..731b354c 100644 --- a/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart +++ b/lib/pages/device_managment/three_gang_switch/view/living_room_device_control.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/bloc/living_room_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/three_gang_switch/factories/living_room_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/three_gang_switch/models/living_room_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -14,7 +15,7 @@ class LivingRoomDeviceControlsView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => LivingRoomBloc(deviceId: deviceId) + create: (context) => LivingRoomBlocFactory.create(deviceId: deviceId) ..add(LivingRoomFetchDeviceStatusEvent(deviceId)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart index 406821da..8f82c198 100644 --- a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart +++ b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart @@ -1,26 +1,33 @@ import 'dart:async'; +import 'dart:developer'; + import 'package:bloc/bloc.dart'; +import 'package:equatable/equatable.dart'; import 'package:firebase_database/firebase_database.dart'; -import 'package:meta/meta.dart'; +import 'package:flutter/foundation.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; + part 'two_gang_glass_switch_event.dart'; part 'two_gang_glass_switch_state.dart'; class TwoGangGlassSwitchBloc extends Bloc { - TwoGangGlassStatusModel deviceStatus; - Timer? _timer; - TwoGangGlassSwitchBloc({required String deviceId}) - : deviceStatus = TwoGangGlassStatusModel( - uuid: deviceId, - switch1: false, - countDown1: 0, - switch2: false, - countDown2: 0), - super(TwoGangGlassSwitchInitial()) { + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; + + late TwoGangGlassStatusModel deviceStatus; + + TwoGangGlassSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(TwoGangGlassSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onBatchControl); @@ -29,14 +36,14 @@ class TwoGangGlassSwitchBloc on(_onStatusUpdated); } - Future _onFetchDeviceStatus(TwoGangGlassSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + TwoGangGlassSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(TwoGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - deviceStatus = - TwoGangGlassStatusModel.fromJson(event.deviceId, status.status); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = TwoGangGlassStatusModel.fromJson(event.deviceId, status.status); _listenToChanges(event.deviceId); emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); } catch (e) { @@ -46,200 +53,121 @@ class TwoGangGlassSwitchBloc void _listenToChanges(String deviceId) { try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - ref.onValue.listen((DatabaseEvent event) { - if (event.snapshot.value == null) return; + final ref = FirebaseDatabase.instance.ref( + 'device-status/$deviceId', + ); + + ref.onValue.listen((event) { + final eventsMap = event.snapshot.value as Map; - Map data = - event.snapshot.value as Map; List statusList = []; - - data['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); + eventsMap['status'].forEach((element) { + statusList.add(Status(code: element['code'], value: element['value'])); }); - // Parse the new status and add the event - final updatedStatus = - TwoGangGlassStatusModel.fromJson(data['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(updatedStatus)); - } + deviceStatus = TwoGangGlassStatusModel.fromJson(deviceId, statusList); + add(StatusUpdated(deviceStatus)); }); - } catch (e) { - // Handle errors and emit an error state if necessary - if (!isClosed) { - // add(TwoGangGlassSwitchError('Error listening to updates: $e')); - } + } catch (_) { + log( + 'Error listening to changes', + name: 'TwoGangGlassSwitchBloc._listenToChanges', + ); } } - Future _onControl(TwoGangGlassSwitchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onControl( + TwoGangGlassSwitchControl event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangGlassSwitchError(e.toString())); + } } - Future _onBatchControl(TwoGangGlassSwitchBatchControl event, - Emitter emit) async { - final oldValue = _getValueByCode(event.code); - + Future _onBatchControl( + TwoGangGlassSwitchBatchControl event, + Emitter emit, + ) async { + emit(TwoGangGlassSwitchLoading()); _updateLocalValue(event.code, event.value); emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangGlassSwitchError(e.toString())); + } } Future _onFetchBatchStatus( - TwoGangGlassSwitchFetchBatchStatusEvent event, - Emitter emit) async { + TwoGangGlassSwitchFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(TwoGangGlassSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.deviceIds); + final status = await DevicesManagementApi().getBatchStatus(event.deviceIds); deviceStatus = TwoGangGlassStatusModel.fromJson( - event.deviceIds.first, status.status); + event.deviceIds.first, + status.status, + ); emit(TwoGangGlassSwitchBatchStatusLoaded(deviceStatus)); } catch (e) { emit(TwoGangGlassSwitchError(e.toString())); } } - Future _onFactoryReset(TwoGangGlassFactoryReset event, - Emitter emit) async { + Future _onFactoryReset( + TwoGangGlassFactoryReset event, + Emitter emit, + ) async { emit(TwoGangGlassSwitchLoading()); try { - final response = await DevicesManagementApi() - .factoryReset(event.factoryReset, event.deviceId); + final response = await DevicesManagementApi().factoryReset( + event.factoryReset, + event.deviceId, + ); if (!response) { - emit(TwoGangGlassSwitchError('Failed')); + emit(TwoGangGlassSwitchError('Failed to reset device')); } else { - emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); + add(TwoGangGlassSwitchFetchDeviceEvent(event.deviceId)); } } catch (e) { emit(TwoGangGlassSwitchError(e.toString())); } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { + deviceStatus = event.deviceStatus; emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); } void _updateLocalValue(String code, bool value) { - if (code == 'switch_1') { - deviceStatus = deviceStatus.copyWith(switch1: value); - } else if (code == 'switch_2') { - deviceStatus = deviceStatus.copyWith(switch2: value); - } - } - - bool _getValueByCode(String code) { switch (code) { case 'switch_1': - return deviceStatus.switch1; + deviceStatus = deviceStatus.copyWith(switch1: value); + break; case 'switch_2': - return deviceStatus.switch2; - default: - return false; + deviceStatus = deviceStatus.copyWith(switch2: value); + break; } } - - @override - Future close() { - _timer?.cancel(); - return super.close(); - } - - // _listenToChanges(deviceId) { - // try { - // DatabaseReference ref = - // FirebaseDatabase.instance.ref('device-status/$deviceId'); - // Stream stream = ref.onValue; - - // stream.listen((DatabaseEvent event) { - // Map usersMap = - // event.snapshot.value as Map; - - // List statusList = []; - // usersMap['status'].forEach((element) { - // statusList - // .add(Status(code: element['code'], value: element['value'])); - // }); - - // deviceStatus = TwoGangGlassStatusModel.fromJson( - // usersMap['productUuid'], statusList); - // if (!isClosed) { - // add(StatusUpdated(deviceStatus)); - // } - // }); - // } catch (_) {} - // } - - void _onStatusUpdated( - StatusUpdated event, Emitter emit) { - // Update the local deviceStatus with the new status from the event - deviceStatus = event.deviceStatus; - // Emit the new state with the updated status - emit(TwoGangGlassSwitchStatusLoaded(deviceStatus)); - } } diff --git a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart index 02b61bd0..46444cce 100644 --- a/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart +++ b/lib/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_event.dart @@ -1,12 +1,17 @@ part of 'two_gang_glass_switch_bloc.dart'; @immutable -abstract class TwoGangGlassSwitchEvent {} +abstract class TwoGangGlassSwitchEvent extends Equatable { + const TwoGangGlassSwitchEvent(); +} class TwoGangGlassSwitchFetchDeviceEvent extends TwoGangGlassSwitchEvent { final String deviceId; - TwoGangGlassSwitchFetchDeviceEvent(this.deviceId); + const TwoGangGlassSwitchFetchDeviceEvent(this.deviceId); + + @override + List get props => [deviceId]; } class TwoGangGlassSwitchControl extends TwoGangGlassSwitchEvent { @@ -14,11 +19,14 @@ class TwoGangGlassSwitchControl extends TwoGangGlassSwitchEvent { final String code; final bool value; - TwoGangGlassSwitchControl({ + const TwoGangGlassSwitchControl({ required this.deviceId, required this.code, required this.value, }); + + @override + List get props => [deviceId, code, value]; } class TwoGangGlassSwitchBatchControl extends TwoGangGlassSwitchEvent { @@ -26,33 +34,43 @@ class TwoGangGlassSwitchBatchControl extends TwoGangGlassSwitchEvent { final String code; final bool value; - TwoGangGlassSwitchBatchControl({ + const TwoGangGlassSwitchBatchControl({ required this.deviceIds, required this.code, required this.value, }); + + @override + List get props => [deviceIds, code, value]; } class TwoGangGlassSwitchFetchBatchStatusEvent extends TwoGangGlassSwitchEvent { final List deviceIds; - TwoGangGlassSwitchFetchBatchStatusEvent(this.deviceIds); + const TwoGangGlassSwitchFetchBatchStatusEvent(this.deviceIds); + + @override + List get props => [deviceIds]; } class TwoGangGlassFactoryReset extends TwoGangGlassSwitchEvent { final String deviceId; final FactoryResetModel factoryReset; - TwoGangGlassFactoryReset({ + const TwoGangGlassFactoryReset({ required this.deviceId, required this.factoryReset, }); + + @override + List get props => [deviceId, factoryReset]; } class StatusUpdated extends TwoGangGlassSwitchEvent { final TwoGangGlassStatusModel deviceStatus; - StatusUpdated(this.deviceStatus); + + const StatusUpdated(this.deviceStatus); + @override List get props => [deviceStatus]; } - diff --git a/lib/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart b/lib/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart new file mode 100644 index 00000000..bd832d8f --- /dev/null +++ b/lib/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart'; + +abstract final class TwoGangGlassSwitchBlocFactory { + const TwoGangGlassSwitchBlocFactory._(); + + static TwoGangGlassSwitchBloc create({ + required String deviceId, + }) { + return TwoGangGlassSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart index c84c1d07..9d120ad6 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_batch_control_view.dart @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_ // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -16,7 +17,7 @@ class TwoGangGlassSwitchBatchControlView extends StatelessWidget with HelperResp @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangGlassSwitchBloc(deviceId: deviceIds.first) + create: (context) => TwoGangGlassSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(TwoGangGlassSwitchFetchBatchStatusEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart index cca794e9..575deeac 100644 --- a/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart +++ b/lib/pages/device_managment/two_g_glass_switch/view/two_gang_glass_switch_control_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/bloc/two_gang_glass_switch_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/factories/two_gang_glass_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_g_glass_switch/models/two_gang_glass_status_model.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -15,7 +16,7 @@ class TwoGangGlassSwitchControlView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangGlassSwitchBloc(deviceId: deviceId) + create: (context) => TwoGangGlassSwitchBlocFactory.create(deviceId: deviceId) ..add(TwoGangGlassSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart index ea72e05b..2e3a8633 100644 --- a/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart +++ b/lib/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:developer'; import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -6,10 +7,22 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_sta import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; class TwoGangSwitchBloc extends Bloc { - TwoGangSwitchBloc({required this.deviceId}) : super(TwoGangSwitchInitial()) { + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; + + late TwoGangStatusModel deviceStatus; + + TwoGangSwitchBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(TwoGangSwitchInitial()) { on(_onFetchDeviceStatus); on(_onControl); on(_onFetchBatchStatus); @@ -18,16 +31,13 @@ class TwoGangSwitchBloc extends Bloc { on(_onStatusUpdated); } - late TwoGangStatusModel deviceStatus; - final String deviceId; - Timer? _timer; - - FutureOr _onFetchDeviceStatus(TwoGangSwitchFetchDeviceEvent event, - Emitter emit) async { + Future _onFetchDeviceStatus( + TwoGangSwitchFetchDeviceEvent event, + Emitter emit, + ) async { emit(TwoGangSwitchLoading()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); deviceStatus = TwoGangStatusModel.fromJson(event.deviceId, status.status); _listenToChanges(event.deviceId); emit(TwoGangSwitchStatusLoaded(deviceStatus)); @@ -36,131 +46,91 @@ class TwoGangSwitchBloc extends Bloc { } } - FutureOr _onControl( - TwoGangSwitchControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); + void _listenToChanges(String deviceId) { + try { + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); + ref.onValue.listen((event) { + final eventsMap = event.snapshot.value as Map; + + List statusList = []; + eventsMap['status'].forEach((element) { + statusList.add( + Status(code: element['code'], value: element['value']), + ); + }); + + deviceStatus = TwoGangStatusModel.fromJson(deviceId, statusList); + add(StatusUpdated(deviceStatus)); + }); + } catch (_) { + log( + 'Error listening to changes', + name: 'TwoGangSwitchBloc._listenToChanges', + ); + } + } + + Future _onControl( + TwoGangSwitchControl event, + Emitter emit, + ) async { + emit(TwoGangSwitchLoading()); _updateLocalValue(event.code, event.value); - emit(TwoGangSwitchStatusLoaded(deviceStatus)); - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, - ); + try { + await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangSwitchError(e.toString())); + } } - Future _runDebounce({ - required dynamic deviceId, - required String code, - required bool value, - required bool oldValue, - required Emitter emit, - required bool isBatch, - }) async { - late String id; - - if (deviceId is List) { - id = deviceId.first; - } else { - id = deviceId; - } - - if (_timer != null) { - _timer!.cancel(); - } - - _timer = Timer(const Duration(milliseconds: 500), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - _revertValueAndEmit(id, code, oldValue, emit); - } - } catch (e) { - _revertValueAndEmit(id, code, oldValue, emit); - } - }); - } - - void _revertValueAndEmit(String deviceId, String code, bool oldValue, - Emitter emit) { - _updateLocalValue(code, oldValue); + Future _onBatchControl( + TwoGangSwitchBatchControl event, + Emitter emit, + ) async { + emit(TwoGangSwitchLoading()); + _updateLocalValue(event.code, event.value); emit(TwoGangSwitchStatusLoaded(deviceStatus)); - } - void _updateLocalValue(String code, bool value) { - if (code == 'switch_1') { - deviceStatus = deviceStatus.copyWith(switch1: value); - } - - if (code == 'switch_2') { - deviceStatus = deviceStatus.copyWith(switch2: value); + try { + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceId, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(TwoGangSwitchError(e.toString())); } } - bool _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.switch1; - case 'switch_2': - return deviceStatus.switch2; - default: - return false; - } - } - - Future _onFetchBatchStatus(TwoGangSwitchFetchBatchEvent event, - Emitter emit) async { + Future _onFetchBatchStatus( + TwoGangSwitchFetchBatchEvent event, + Emitter emit, + ) async { emit(TwoGangSwitchLoading()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesIds); - deviceStatus = - TwoGangStatusModel.fromJson(event.devicesIds.first, status.status); + final status = await DevicesManagementApi().getBatchStatus(event.devicesIds); + deviceStatus = TwoGangStatusModel.fromJson( + event.devicesIds.first, + status.status, + ); emit(TwoGangSwitchStatusLoaded(deviceStatus)); } catch (e) { emit(TwoGangSwitchError(e.toString())); } } - @override - Future close() { - _timer?.cancel(); - return super.close(); - } - - FutureOr _onBatchControl( - TwoGangSwitchBatchControl event, Emitter emit) async { - final oldValue = _getValueByCode(event.code); - - _updateLocalValue(event.code, event.value); - - emit(TwoGangSwitchStatusLoaded(deviceStatus)); - - await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, - ); - } - - FutureOr _onFactoryReset( - TwoGangFactoryReset event, Emitter emit) async { + Future _onFactoryReset( + TwoGangFactoryReset event, + Emitter emit, + ) async { emit(TwoGangSwitchLoading()); try { final response = await DevicesManagementApi().factoryReset( @@ -168,42 +138,31 @@ class TwoGangSwitchBloc extends Bloc { event.deviceId, ); if (!response) { - emit(TwoGangSwitchError('Failed')); + emit(TwoGangSwitchError('Failed to reset device')); } else { - emit(TwoGangSwitchStatusLoaded(deviceStatus)); + add(TwoGangSwitchFetchDeviceEvent(event.deviceId)); } } catch (e) { emit(TwoGangSwitchError(e.toString())); } } - _listenToChanges(deviceId) { - try { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - Stream stream = ref.onValue; - - stream.listen((DatabaseEvent event) { - Map usersMap = - event.snapshot.value as Map; - - List statusList = []; - usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); - }); - - deviceStatus = - TwoGangStatusModel.fromJson(usersMap['productUuid'], statusList); - if (!isClosed) { - add(StatusUpdated(deviceStatus)); - } - }); - } catch (_) {} - } - - void _onStatusUpdated(StatusUpdated event, Emitter emit) { + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { deviceStatus = event.deviceStatus; emit(TwoGangSwitchStatusLoaded(deviceStatus)); } + + void _updateLocalValue(String code, bool value) { + switch (code) { + case 'switch_1': + deviceStatus = deviceStatus.copyWith(switch1: value); + break; + case 'switch_2': + deviceStatus = deviceStatus.copyWith(switch2: value); + break; + } + } } diff --git a/lib/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart b/lib/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart new file mode 100644 index 00000000..37893caf --- /dev/null +++ b/lib/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; + +abstract final class TwoGangSwitchBlocFactory { + const TwoGangSwitchBlocFactory._(); + + static TwoGangSwitchBloc create({ + required String deviceId, + }) { + return TwoGangSwitchBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart b/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart index 6cec4256..58094a71 100644 --- a/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart +++ b/lib/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart @@ -24,16 +24,16 @@ class TwoGangStatusModel { for (var status in jsonList) { switch (status.code) { case 'switch_1': - switch1 = status.value ?? false; + switch1 = bool.tryParse(status.value.toString()) ?? false; break; case 'countdown_1': - countDown = status.value ?? 0; + countDown = int.tryParse(status.value.toString()) ?? 0; break; case 'switch_2': - switch2 = status.value ?? false; + switch2 = bool.tryParse(status.value.toString()) ?? false; break; case 'countdown_2': - countDown2 = status.value ?? 0; + countDown2 = int.tryParse(status.value.toString()) ?? 0; break; } } diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart index b3a39287..e8346cb2 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_batch_control.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/factory_reset_model.dart'; import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; -// import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -18,7 +18,7 @@ class TwoGangBatchControlView extends StatelessWidget with HelperResponsiveLayou @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangSwitchBloc(deviceId: deviceIds.first) + create: (context) => TwoGangSwitchBlocFactory.create(deviceId: deviceIds.first) ..add(TwoGangSwitchFetchBatchEvent(deviceIds)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart index 840d356e..882aac3e 100644 --- a/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart +++ b/lib/pages/device_managment/two_gang_switch/view/wall_light_device_control.dart @@ -4,6 +4,7 @@ import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_bloc.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_event.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/bloc/two_gang_switch_state.dart'; +import 'package:syncrow_web/pages/device_managment/two_gang_switch/factories/two_gang_switch_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/two_gang_switch/models/two_gang_status_model.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -15,7 +16,7 @@ class TwoGangDeviceControlView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => TwoGangSwitchBloc(deviceId: deviceId) + create: (context) => TwoGangSwitchBlocFactory.create(deviceId: deviceId) ..add(TwoGangSwitchFetchDeviceEvent(deviceId)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart b/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart index 3c144142..630a132b 100644 --- a/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart +++ b/lib/pages/device_managment/wall_sensor/bloc/wall_bloc.dart @@ -1,18 +1,28 @@ import 'dart:async'; +import 'dart:developer'; + import 'package:firebase_database/firebase_database.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/device_status.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_event.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_state.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; class WallSensorBloc extends Bloc { final String deviceId; - late WallSensorModel deviceStatus; - Timer? _timer; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; - WallSensorBloc({required this.deviceId}) : super(WallSensorInitialState()) { + late WallSensorModel deviceStatus; + + WallSensorBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(WallSensorInitialState()) { on(_fetchWallSensorStatus); on(_fetchWallSensorBatchControl); on(_changeValue); @@ -24,28 +34,28 @@ class WallSensorBloc extends Bloc { on(_onRealtimeUpdate); } - void _fetchWallSensorStatus( - WallSensorFetchStatusEvent event, Emitter emit) async { + Future _fetchWallSensorStatus( + WallSensorFetchStatusEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingInitialState()); try { - var response = await DevicesManagementApi().getDeviceStatus(deviceId); + final response = await DevicesManagementApi().getDeviceStatus(deviceId); deviceStatus = WallSensorModel.fromJson(response.status); - emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); _listenToChanges(deviceId); + emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); } catch (e) { emit(WallSensorFailedState(error: e.toString())); - return; } } - // Fetch batch status - FutureOr _fetchWallSensorBatchControl( - WallSensorFetchBatchStatusEvent event, - Emitter emit) async { + Future _fetchWallSensorBatchControl( + WallSensorFetchBatchStatusEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingInitialState()); try { - var response = - await DevicesManagementApi().getBatchStatus(event.devicesIds); + final response = await DevicesManagementApi().getBatchStatus(event.devicesIds); deviceStatus = WallSensorModel.fromJson(response.status); emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); } catch (e) { @@ -54,132 +64,105 @@ class WallSensorBloc extends Bloc { } void _listenToChanges(String deviceId) { - DatabaseReference ref = - FirebaseDatabase.instance.ref('device-status/$deviceId'); - ref.onValue.listen((DatabaseEvent event) { - final data = event.snapshot.value as Map?; - if (data == null) return; + try { + final ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); - final statusList = (data['status'] as List?) - ?.map((e) => Status(code: e['code'], value: e['value'])) - .toList(); + ref.onValue.listen((event) { + final eventsMap = event.snapshot.value as Map; - if (statusList != null) { - final updatedDeviceStatus = WallSensorModel.fromJson(statusList); + List statusList = []; + eventsMap['status'].forEach((element) { + statusList.add( + Status(code: element['code'], value: element['value']), + ); + }); + + deviceStatus = WallSensorModel.fromJson(statusList); if (!isClosed) { - add(WallSensorRealtimeUpdateEvent(updatedDeviceStatus)); + add(WallSensorRealtimeUpdateEvent(deviceStatus)); } - } - }); + }); + } catch (_) { + log( + 'Error listening to changes', + name: 'WallSensorBloc._listenToChanges', + ); + } } - - void _changeValue( - WallSensorChangeValueEvent event, Emitter emit) async { + Future _changeValue( + WallSensorChangeValueEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); - if (event.code == 'far_detection') { - deviceStatus.farDetection = event.value; - } else if (event.code == 'motionless_sensitivity') { - deviceStatus.motionlessSensitivity = event.value; - } else if (event.code == 'motion_sensitivity_value') { - deviceStatus.motionSensitivity = event.value; - } else if (event.code == 'no_one_time') { - deviceStatus.noBodyTime = event.value; - } + _updateLocalValue(event.code, event.value); emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: deviceId, - code: event.code, - value: event.value, - isBatch: false, - emit: emit, - ); + + try { + await controlDeviceService.controlDevice( + deviceUuid: deviceId, + status: Status(code: event.code, value: event.value), + ); + } catch (e) { + _updateLocalValue(event.code, event.value == 0 ? 1 : 0); + emit(WallSensorFailedState(error: e.toString())); + } } Future _onBatchControl( - WallSensorBatchControlEvent event, Emitter emit) async { + WallSensorBatchControlEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); - if (event.code == 'far_detection') { - deviceStatus.farDetection = event.value; - } else if (event.code == 'motionless_sensitivity') { - deviceStatus.motionlessSensitivity = event.value; - } else if (event.code == 'motion_sensitivity_value') { - deviceStatus.motionSensitivity = event.value; - } else if (event.code == 'no_one_time') { - deviceStatus.noBodyTime = event.value; - } + _updateLocalValue(event.code, event.value); emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); - await _runDeBouncer( - deviceId: event.deviceIds, - code: event.code, - value: event.value, - emit: emit, - isBatch: true, - ); - } - - _runDeBouncer({ - required dynamic deviceId, - required String code, - required dynamic value, - required Emitter emit, - required bool isBatch, - }) { - if (_timer != null) { - _timer!.cancel(); - } - _timer = Timer(const Duration(seconds: 1), () async { - try { - late bool response; - if (isBatch) { - response = await DevicesManagementApi() - .deviceBatchControl(deviceId, code, value); - } else { - response = await DevicesManagementApi() - .deviceControl(deviceId, Status(code: code, value: value)); - } - - if (!response) { - add(WallSensorFetchStatusEvent()); - } - } catch (_) { - await Future.delayed(const Duration(milliseconds: 500)); - add(WallSensorFetchStatusEvent()); - } - }); - } - - FutureOr _getDeviceReports( - GetDeviceReportsEvent event, Emitter emit) async { - emit(DeviceReportsLoadingState()); - // final from = DateTime.now().subtract(const Duration(days: 30)).millisecondsSinceEpoch; - // final to = DateTime.now().millisecondsSinceEpoch; try { - // await DevicesManagementApi.getDeviceReportsByDate( - // deviceId, event.code, from.toString(), to.toString()) - await DevicesManagementApi.getDeviceReports(deviceId, event.code) - .then((value) { - emit(DeviceReportsState(deviceReport: value, code: event.code)); - }); + await batchControlDevicesService.batchControlDevices( + uuids: event.deviceIds, + code: event.code, + value: event.value, + ); + } catch (e) { + _updateLocalValue(event.code, !event.value); + emit(WallSensorFailedState(error: e.toString())); + } + } + + Future _getDeviceReports( + GetDeviceReportsEvent event, + Emitter emit, + ) async { + emit(DeviceReportsLoadingState()); + try { + final reports = await DevicesManagementApi.getDeviceReports( + deviceId, + event.code, + ); + emit(DeviceReportsState(deviceReport: reports, code: event.code)); } catch (e) { emit(DeviceReportsFailedState(error: e.toString())); - return; } } void _showDescription( - ShowDescriptionEvent event, Emitter emit) { + ShowDescriptionEvent event, + Emitter emit, + ) { emit(WallSensorShowDescriptionState(description: event.description)); } void _backToGridView( - BackToGridViewEvent event, Emitter emit) { + BackToGridViewEvent event, + Emitter emit, + ) { emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); } - FutureOr _onFactoryReset( - WallSensorFactoryResetEvent event, Emitter emit) async { + Future _onFactoryReset( + WallSensorFactoryResetEvent event, + Emitter emit, + ) async { emit(WallSensorLoadingNewSate(wallSensorModel: deviceStatus)); try { final response = await DevicesManagementApi().factoryReset( @@ -187,9 +170,9 @@ class WallSensorBloc extends Bloc { event.deviceId, ); if (!response) { - emit(const WallSensorFailedState(error: 'Failed')); + emit(const WallSensorFailedState(error: 'Failed to reset device')); } else { - emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); + add(WallSensorFetchStatusEvent()); } } catch (e) { emit(WallSensorFailedState(error: e.toString())); @@ -200,7 +183,23 @@ class WallSensorBloc extends Bloc { WallSensorRealtimeUpdateEvent event, Emitter emit, ) { - deviceStatus = event.deviceStatus; - emit(WallSensorUpdateState(wallSensorModel: deviceStatus)); + emit(WallSensorUpdateState(wallSensorModel: event.deviceStatus)); + } + + void _updateLocalValue(String code, dynamic value) { + switch (code) { + case 'far_detection': + deviceStatus.farDetection = value; + break; + case 'motionless_sensitivity': + deviceStatus.motionlessSensitivity = value; + break; + case 'motion_sensitivity_value': + deviceStatus.motionSensitivity = value; + break; + case 'no_one_time': + deviceStatus.noBodyTime = value; + break; + } } } diff --git a/lib/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart b/lib/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart new file mode 100644 index 00000000..d7811717 --- /dev/null +++ b/lib/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_bloc.dart'; + +abstract final class WallSensorBlocFactory { + const WallSensorBlocFactory._(); + + static WallSensorBloc create({ + required String deviceId, + }) { + return WallSensorBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart b/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart index 27169f0e..61108387 100644 --- a/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart +++ b/lib/pages/device_managment/wall_sensor/view/wall_sensor_batch_control.dart @@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presen import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_bloc.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_event.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/bloc/wall_state.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -21,7 +22,7 @@ class WallSensorBatchControlView extends StatelessWidget with HelperResponsiveLa final isLarge = isLargeScreenSize(context); final isMedium = isMediumScreenSize(context); return BlocProvider( - create: (context) => WallSensorBloc(deviceId: devicesIds.first) + create: (context) => WallSensorBlocFactory.create(deviceId: devicesIds.first) ..add(WallSensorFetchBatchStatusEvent(devicesIds)), child: BlocBuilder( builder: (context, state) { diff --git a/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart b/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart index 370edaa5..def8ed93 100644 --- a/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart +++ b/lib/pages/device_managment/wall_sensor/view/wall_sensor_conrtols.dart @@ -10,6 +10,7 @@ import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presen import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_static_widget.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_status.dart'; import 'package:syncrow_web/pages/device_managment/shared/sensors_widgets/presence_update_data.dart'; +import 'package:syncrow_web/pages/device_managment/wall_sensor/factories/wall_sensor_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/wall_sensor/model/wall_sensor_model.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; @@ -26,7 +27,7 @@ class WallSensorControlsView extends StatelessWidget with HelperResponsiveLayout final isMedium = isMediumScreenSize(context); return BlocProvider( create: (context) => - WallSensorBloc(deviceId: device.uuid!)..add(WallSensorFetchStatusEvent()), + WallSensorBlocFactory.create(deviceId: device.uuid!)..add(WallSensorFetchStatusEvent()), child: BlocBuilder( builder: (context, state) { if (state is WallSensorLoadingInitialState || state is DeviceReportsLoadingState) { diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart index 18a0787f..560a61e1 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_bloc.dart @@ -1,5 +1,3 @@ -// water_heater_bloc.dart - import 'dart:async'; import 'package:bloc/bloc.dart'; @@ -10,6 +8,8 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/device_sta import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_entry.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/schedule_model.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/services/batch_control_devices_service.dart'; +import 'package:syncrow_web/services/control_device_service.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; import 'package:syncrow_web/utils/format_date_time.dart'; @@ -17,7 +17,17 @@ part 'water_heater_event.dart'; part 'water_heater_state.dart'; class WaterHeaterBloc extends Bloc { - WaterHeaterBloc() : super(WaterHeaterInitial()) { + late WaterHeaterStatusModel deviceStatus; + final String deviceId; + final ControlDeviceService controlDeviceService; + final BatchControlDevicesService batchControlDevicesService; + Timer? _countdownTimer; + + WaterHeaterBloc({ + required this.deviceId, + required this.controlDeviceService, + required this.batchControlDevicesService, + }) : super(WaterHeaterInitial()) { on(_fetchWaterHeaterStatus); on(_controlWaterHeater); on(_batchFetchWaterHeater); @@ -29,7 +39,6 @@ class WaterHeaterBloc extends Bloc { on(_updateSelectedTime); on(_updateSelectedDay); on(_updateFunctionOn); - on(_getSchedule); on(_onAddSchedule); on(_onEditSchedule); @@ -38,11 +47,7 @@ class WaterHeaterBloc extends Bloc { on(_onStatusUpdated); } - late WaterHeaterStatusModel deviceStatus; - Timer? _countdownTimer; - // Timer? _inchingTimer; - - FutureOr _initializeAddSchedule( + void _initializeAddSchedule( InitializeAddScheduleEvent event, Emitter emit, ) { @@ -64,7 +69,7 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _updateSelectedTime( + void _updateSelectedTime( UpdateSelectedTimeEvent event, Emitter emit, ) { @@ -73,7 +78,7 @@ class WaterHeaterBloc extends Bloc { emit(currentState.copyWith(selectedTime: event.selectedTime)); } - FutureOr _updateSelectedDay( + void _updateSelectedDay( UpdateSelectedDayEvent event, Emitter emit, ) { @@ -84,7 +89,7 @@ class WaterHeaterBloc extends Bloc { selectedDays: updatedDays, selectedTime: currentState.selectedTime)); } - FutureOr _updateFunctionOn( + void _updateFunctionOn( UpdateFunctionOnEvent event, Emitter emit, ) { @@ -93,16 +98,18 @@ class WaterHeaterBloc extends Bloc { functionOn: event.isOn, selectedTime: currentState.selectedTime)); } - FutureOr _updateScheduleEvent( + Future _updateScheduleEvent( UpdateScheduleEvent event, Emitter emit, ) async { final currentState = state; if (currentState is WaterHeaterDeviceStatusLoaded) { if (event.scheduleMode == ScheduleModes.schedule) { - emit(currentState.copyWith( - scheduleMode: ScheduleModes.schedule, - )); + emit( + currentState.copyWith( + scheduleMode: ScheduleModes.schedule, + ), + ); } if (event.scheduleMode == ScheduleModes.countdown) { final countdownRemaining = @@ -116,87 +123,88 @@ class WaterHeaterBloc extends Bloc { countdownRemaining: countdownRemaining, )); - if (!currentState.isCountdownActive! && - countdownRemaining > Duration.zero) { + if (!currentState.isCountdownActive! && countdownRemaining > Duration.zero) { _startCountdownTimer(emit, countdownRemaining); } } else if (event.scheduleMode == ScheduleModes.inching) { - final inchingDuration = - Duration(hours: event.hours, minutes: event.minutes); + final inchingDuration = Duration(hours: event.hours, minutes: event.minutes); - emit(currentState.copyWith( - scheduleMode: ScheduleModes.inching, - inchingHours: inchingDuration.inHours, - inchingMinutes: inchingDuration.inMinutes % 60, - isInchingActive: currentState.isInchingActive, - )); + emit( + currentState.copyWith( + scheduleMode: ScheduleModes.inching, + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: currentState.isInchingActive, + ), + ); } } } - FutureOr _controlWaterHeater( + Future _controlWaterHeater( ToggleWaterHeaterEvent event, Emitter emit, ) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; - final oldValue = _getValueByCode(event.code); - _updateLocalValue(event.code, event.value); - emit(currentState.copyWith( - status: deviceStatus, - )); + emit( + currentState.copyWith( + status: deviceStatus, + ), + ); - final success = await _runDebounce( - deviceId: event.deviceId, - code: event.code, - value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: false, + final success = await controlDeviceService.controlDevice( + deviceUuid: event.deviceId, + status: Status( + code: event.code, + value: event.value, + ), ); if (success) { if (event.code == "countdown_1") { final countdownDuration = Duration(seconds: event.value); - emit(currentState.copyWith( - countdownHours: countdownDuration.inHours, - countdownMinutes: countdownDuration.inMinutes % 60, - countdownRemaining: countdownDuration, - isCountdownActive: true, - )); + emit( + currentState.copyWith( + countdownHours: countdownDuration.inHours, + countdownMinutes: countdownDuration.inMinutes % 60, + countdownRemaining: countdownDuration, + isCountdownActive: true, + ), + ); if (countdownDuration.inSeconds > 0) { _startCountdownTimer(emit, countdownDuration); } else { _countdownTimer?.cancel(); - emit(currentState.copyWith( - countdownHours: 0, - countdownMinutes: 0, - countdownRemaining: Duration.zero, - isCountdownActive: false, - )); + emit( + currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + ), + ); } } else if (event.code == "switch_inching") { final inchingDuration = Duration(seconds: event.value); - //if (inchingDuration.inSeconds > 0) { - // _startInchingTimer(emit, inchingDuration); - // } else { - emit(currentState.copyWith( - inchingHours: inchingDuration.inHours, - inchingMinutes: inchingDuration.inMinutes % 60, - isInchingActive: true, - )); - // } + emit( + currentState.copyWith( + inchingHours: inchingDuration.inHours, + inchingMinutes: inchingDuration.inMinutes % 60, + isInchingActive: true, + ), + ); } } } } - FutureOr _stopScheduleEvent( + Future _stopScheduleEvent( StopScheduleEvent event, Emitter emit, ) async { @@ -207,25 +215,28 @@ class WaterHeaterBloc extends Bloc { _countdownTimer?.cancel(); if (isCountDown) { - emit(currentState.copyWith( - countdownHours: 0, - countdownMinutes: 0, - countdownRemaining: Duration.zero, - isCountdownActive: false, - )); + emit( + currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + countdownRemaining: Duration.zero, + isCountdownActive: false, + ), + ); } else if (currentState.scheduleMode == ScheduleModes.inching) { - emit(currentState.copyWith( - inchingHours: 0, - inchingMinutes: 0, - isInchingActive: false, - )); + emit( + currentState.copyWith( + inchingHours: 0, + inchingMinutes: 0, + isInchingActive: false, + ), + ); } try { final status = await DevicesManagementApi().deviceControl( event.deviceId, - Status( - code: isCountDown ? 'countdown_1' : 'switch_inching', value: 0), + Status(code: isCountDown ? 'countdown_1' : 'switch_inching', value: 0), ); if (!status) { emit(const WaterHeaterFailedState(error: 'Failed to stop schedule.')); @@ -236,17 +247,15 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _fetchWaterHeaterStatus( + Future _fetchWaterHeaterStatus( WaterHeaterFetchStatusEvent event, Emitter emit, ) async { emit(WaterHeaterLoadingState()); try { - final status = - await DevicesManagementApi().getDeviceStatus(event.deviceId); - deviceStatus = - WaterHeaterStatusModel.fromJson(event.deviceId, status.status); + final status = await DevicesManagementApi().getDeviceStatus(event.deviceId); + deviceStatus = WaterHeaterStatusModel.fromJson(event.deviceId, status.status); if (deviceStatus.scheduleMode == ScheduleModes.countdown) { final countdownRemaining = Duration( @@ -288,7 +297,6 @@ class WaterHeaterBloc extends Bloc { inchingMinutes: deviceStatus.inchingMinutes, isInchingActive: true, )); -//_startInchingTimer(emit, inchingDuration); } else { emit(WaterHeaterDeviceStatusLoaded( deviceStatus, @@ -316,7 +324,7 @@ class WaterHeaterBloc extends Bloc { } } - _listenToChanges(deviceId) { + void _listenToChanges(deviceId) { try { DatabaseReference ref = FirebaseDatabase.instance.ref('device-status/$deviceId'); @@ -328,12 +336,11 @@ class WaterHeaterBloc extends Bloc { List statusList = []; usersMap['status'].forEach((element) { - statusList - .add(Status(code: element['code'], value: element['value'])); + statusList.add(Status(code: element['code'], value: element['value'])); }); - deviceStatus = WaterHeaterStatusModel.fromJson( - usersMap['productUuid'], statusList); + deviceStatus = + WaterHeaterStatusModel.fromJson(usersMap['productUuid'], statusList); if (!isClosed) { add(StatusUpdated(deviceStatus)); } @@ -341,7 +348,10 @@ class WaterHeaterBloc extends Bloc { } catch (_) {} } - void _onStatusUpdated(StatusUpdated event, Emitter emit) { + void _onStatusUpdated( + StatusUpdated event, + Emitter emit, + ) { deviceStatus = event.deviceStatus; emit(WaterHeaterDeviceStatusLoaded(deviceStatus)); } @@ -352,23 +362,13 @@ class WaterHeaterBloc extends Bloc { ) { _countdownTimer?.cancel(); - _countdownTimer = Timer.periodic(const Duration(minutes: 1), (timer) { - add(DecrementCountdownEvent()); - }); + _countdownTimer = Timer.periodic( + const Duration(minutes: 1), + (timer) => add(DecrementCountdownEvent()), + ); } - // void _startInchingTimer( - // Emitter emit, - // Duration inchingDuration, - // ) { - // _inchingTimer?.cancel(); - - // _inchingTimer = Timer.periodic(const Duration(minutes: 1), (timer) { - // add(DecrementInchingEvent()); - // }); - // } - - _onDecrementCountdown( + void _onDecrementCountdown( DecrementCountdownEvent event, Emitter emit, ) { @@ -382,105 +382,29 @@ class WaterHeaterBloc extends Bloc { if (newRemaining <= Duration.zero) { _countdownTimer?.cancel(); - emit(currentState.copyWith( - countdownHours: 0, - countdownMinutes: 0, - isCountdownActive: false, - countdownRemaining: Duration.zero, - )); + emit( + currentState.copyWith( + countdownHours: 0, + countdownMinutes: 0, + isCountdownActive: false, + countdownRemaining: Duration.zero, + ), + ); return; } - int totalSeconds = newRemaining.inSeconds; + final totalSeconds = newRemaining.inSeconds; + final newHours = totalSeconds ~/ 3600; + final newMinutes = (totalSeconds % 3600) ~/ 60; - int newHours = totalSeconds ~/ 3600; - int newMinutes = (totalSeconds % 3600) ~/ 60; - - emit(currentState.copyWith( - countdownHours: newHours, - countdownMinutes: newMinutes, - countdownRemaining: newRemaining, - )); - } - } - } - - // FutureOr _onDecrementInching( - // DecrementInchingEvent event, - // Emitter emit, - // ) { - // if (state is WaterHeaterDeviceStatusLoaded) { - // final currentState = state as WaterHeaterDeviceStatusLoaded; - - // if (currentState.inchingHours > 0 || currentState.inchingMinutes > 0) { - // final newRemaining = Duration( - // hours: currentState.inchingHours, - // minutes: currentState.inchingMinutes, - // ) - - // const Duration(minutes: 1); - - // if (newRemaining <= Duration.zero) { - // _inchingTimer?.cancel(); - // emit(currentState.copyWith( - // inchingHours: 0, - // inchingMinutes: 0, - // isInchingActive: false, - // )); - // } else { - // emit(currentState.copyWith( - // inchingHours: newRemaining.inHours, - // inchingMinutes: newRemaining.inMinutes % 60, - // )); - // } - // } - // } - // } - - Future _runDebounce({ - required dynamic deviceId, - required String code, - required dynamic value, - required dynamic oldValue, - required Emitter emit, - required bool isBatch, - }) async { - try { - late bool status; - await Future.delayed(const Duration(milliseconds: 500)); - - if (isBatch) { - status = await DevicesManagementApi().deviceBatchControl( - deviceId, - code, - value, - ); - } else { - status = await DevicesManagementApi().deviceControl( - deviceId, - Status(code: code, value: value), + emit( + currentState.copyWith( + countdownHours: newHours, + countdownMinutes: newMinutes, + countdownRemaining: newRemaining, + ), ); } - - if (!status) { - _revertValue(code, oldValue, emit.call); - return false; - } else { - return true; - } - } catch (e) { - _revertValue(code, oldValue, emit.call); - return false; - } - } - - void _revertValue(String code, dynamic oldValue, - void Function(WaterHeaterState state) emit) { - _updateLocalValue(code, oldValue); - if (state is WaterHeaterDeviceStatusLoaded) { - final currentState = state as WaterHeaterDeviceStatusLoaded; - emit(currentState.copyWith( - status: deviceStatus, - )); } } @@ -505,14 +429,12 @@ class WaterHeaterBloc extends Bloc { } dynamic _getValueByCode(String code) { - switch (code) { - case 'switch_1': - return deviceStatus.heaterSwitch; - case 'countdown_1': - return deviceStatus.countdownHours * 60 + deviceStatus.countdownMinutes; - default: - return null; - } + return switch (code) { + 'switch_1' => deviceStatus.heaterSwitch, + 'countdown_1' => + (deviceStatus.countdownHours * 60) + deviceStatus.countdownMinutes, + _ => null, + }; } @override @@ -521,13 +443,17 @@ class WaterHeaterBloc extends Bloc { return super.close(); } - FutureOr _getSchedule( - GetSchedulesEvent event, Emitter emit) async { + Future _getSchedule( + GetSchedulesEvent event, + Emitter emit, + ) async { emit(ScheduleLoadingState()); try { - List schedules = await DevicesManagementApi() - .getDeviceSchedules(deviceStatus.uuid, event.category); + final schedules = await DevicesManagementApi().getDeviceSchedules( + deviceStatus.uuid, + event.category, + ); emit(WaterHeaterDeviceStatusLoaded( deviceStatus, @@ -535,7 +461,6 @@ class WaterHeaterBloc extends Bloc { scheduleMode: ScheduleModes.schedule, )); } catch (e) { - //(const WaterHeaterFailedState(error: 'Failed to fetch schedules.')); emit(WaterHeaterDeviceStatusLoaded( deviceStatus, schedules: const [], @@ -543,7 +468,7 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _onAddSchedule( + Future _onAddSchedule( AddScheduleEvent event, Emitter emit, ) async { @@ -557,8 +482,6 @@ class WaterHeaterBloc extends Bloc { days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays), ); - // emit(ScheduleLoadingState()); - bool success = await DevicesManagementApi() .addScheduleRecord(newSchedule, currentState.status.uuid); @@ -566,13 +489,14 @@ class WaterHeaterBloc extends Bloc { add(GetSchedulesEvent(category: 'switch_1', uuid: deviceStatus.uuid)); } else { emit(currentState); - //emit(const WaterHeaterFailedState(error: 'Failed to add schedule.')); } } } - FutureOr _onEditSchedule(EditWaterHeaterScheduleEvent event, - Emitter emit) async { + Future _onEditSchedule( + EditWaterHeaterScheduleEvent event, + Emitter emit, + ) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; @@ -584,8 +508,6 @@ class WaterHeaterBloc extends Bloc { days: ScheduleModel.convertSelectedDaysToStrings(event.selectedDays), ); - // emit(ScheduleLoadingState()); - bool success = await DevicesManagementApi().editScheduleRecord( currentState.status.uuid, newSchedule, @@ -595,12 +517,11 @@ class WaterHeaterBloc extends Bloc { add(GetSchedulesEvent(category: 'switch_1', uuid: deviceStatus.uuid)); } else { emit(currentState); - //emit(const WaterHeaterFailedState(error: 'Failed to add schedule.')); } } } - FutureOr _onUpdateSchedule( + Future _onUpdateSchedule( UpdateScheduleEntryEvent event, Emitter emit, ) async { @@ -627,20 +548,17 @@ class WaterHeaterBloc extends Bloc { emit(currentState.copyWith(schedules: updatedSchedules)); } else { emit(currentState); - // emit(const WaterHeaterFailedState(error: 'Failed to update schedule.')); } } } - FutureOr _onDeleteSchedule( + Future _onDeleteSchedule( DeleteScheduleEvent event, Emitter emit, ) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; - // emit(ScheduleLoadingState()); - bool success = await DevicesManagementApi() .deleteScheduleRecord(currentState.status.uuid, event.scheduleId); @@ -652,20 +570,22 @@ class WaterHeaterBloc extends Bloc { emit(currentState.copyWith(schedules: updatedSchedules)); } else { emit(currentState); - // emit(const WaterHeaterFailedState(error: 'Failed to delete schedule.')); } } } - FutureOr _batchFetchWaterHeater(FetchWaterHeaterBatchStatusEvent event, - Emitter emit) async { + Future _batchFetchWaterHeater( + FetchWaterHeaterBatchStatusEvent event, + Emitter emit, + ) async { emit(WaterHeaterLoadingState()); try { - final status = - await DevicesManagementApi().getBatchStatus(event.devicesUuid); - deviceStatus = WaterHeaterStatusModel.fromJson( - event.devicesUuid.first, status.status); + final status = await DevicesManagementApi().getBatchStatus( + event.devicesUuid, + ); + deviceStatus = + WaterHeaterStatusModel.fromJson(event.devicesUuid.first, status.status); emit(WaterHeaterDeviceStatusLoaded(deviceStatus)); } catch (e) { @@ -673,8 +593,8 @@ class WaterHeaterBloc extends Bloc { } } - FutureOr _batchControlWaterHeater(ControlWaterHeaterBatchEvent event, - Emitter emit) async { + Future _batchControlWaterHeater( + ControlWaterHeaterBatchEvent event, Emitter emit) async { if (state is WaterHeaterDeviceStatusLoaded) { final currentState = state as WaterHeaterDeviceStatusLoaded; @@ -686,13 +606,10 @@ class WaterHeaterBloc extends Bloc { status: deviceStatus, )); - final success = await _runDebounce( - deviceId: event.devicesUuid, + final success = await batchControlDevicesService.batchControlDevices( + uuids: event.devicesUuid, code: event.code, value: event.value, - oldValue: oldValue, - emit: emit, - isBatch: true, ); if (success) { diff --git a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart index c2df43c3..974f5f2d 100644 --- a/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart +++ b/lib/pages/device_managment/water_heater/bloc/water_heater_state.dart @@ -1,5 +1,3 @@ -// water_heater_state.dart - part of 'water_heater_bloc.dart'; sealed class WaterHeaterState extends Equatable { diff --git a/lib/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart b/lib/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart new file mode 100644 index 00000000..9c0c8ab6 --- /dev/null +++ b/lib/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart @@ -0,0 +1,18 @@ +import 'package:syncrow_web/pages/device_managment/factories/device_bloc_dependencies_factory.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; + +abstract final class WaterHeaterBlocFactory { + const WaterHeaterBlocFactory._(); + + static WaterHeaterBloc create({ + required String deviceId, + }) { + return WaterHeaterBloc( + deviceId: deviceId, + controlDeviceService: + DeviceBlocDependenciesFactory.createControlDeviceService(), + batchControlDevicesService: + DeviceBlocDependenciesFactory.createBatchControlDevicesService(), + ); + } +} diff --git a/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart index aaab5271..3c8a3858 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_batch_control.dart @@ -1,15 +1,16 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; - import 'package:syncrow_web/pages/device_managment/shared/batch_control/factory_reset.dart'; // import 'package:syncrow_web/pages/device_managment/shared/batch_control/firmware_update.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; -class WaterHEaterBatchControlView extends StatelessWidget with HelperResponsiveLayout { +class WaterHEaterBatchControlView extends StatelessWidget + with HelperResponsiveLayout { const WaterHEaterBatchControlView({super.key, required this.deviceIds}); final List deviceIds; @@ -17,8 +18,9 @@ class WaterHEaterBatchControlView extends StatelessWidget with HelperResponsiveL @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => - WaterHeaterBloc()..add(FetchWaterHeaterBatchStatusEvent(devicesUuid: deviceIds)), + create: (context) => WaterHeaterBlocFactory.create( + deviceId: deviceIds.first, + )..add(FetchWaterHeaterBatchStatusEvent(devicesUuid: deviceIds)), child: BlocBuilder( builder: (context, state) { if (state is WaterHeaterLoadingState) { diff --git a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart index 40d3edb5..f1e56136 100644 --- a/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart +++ b/lib/pages/device_managment/water_heater/view/water_heater_device_control.dart @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo import 'package:syncrow_web/pages/device_managment/shared/device_controls_container.dart'; import 'package:syncrow_web/pages/device_managment/shared/toggle_widget.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/bloc/water_heater_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/water_heater/factories/water_heater_bloc_factory.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; import 'package:syncrow_web/pages/device_managment/water_heater/widgets/schedual_view.dart'; import 'package:syncrow_web/utils/color_manager.dart'; @@ -21,8 +22,9 @@ class WaterHeaterDeviceControlView extends StatelessWidget @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => - WaterHeaterBloc()..add(WaterHeaterFetchStatusEvent(device.uuid!)), + create: (context) => WaterHeaterBlocFactory.create( + deviceId: device.uuid ?? '', + )..add(WaterHeaterFetchStatusEvent(device.uuid!)), child: BlocBuilder( builder: (context, state) { if (state is WaterHeaterLoadingState) { @@ -33,8 +35,7 @@ class WaterHeaterDeviceControlView extends StatelessWidget state is WaterHeaterBatchFailedState) { return const Center(child: Text('Error fetching status')); } else { - return const SizedBox( - height: 200, child: Center(child: SizedBox())); + return const SizedBox(height: 200, child: Center(child: SizedBox())); } }, )); diff --git a/lib/pages/home/bloc/home_bloc.dart b/lib/pages/home/bloc/home_bloc.dart index df9304bc..cb3e75f0 100644 --- a/lib/pages/home/bloc/home_bloc.dart +++ b/lib/pages/home/bloc/home_bloc.dart @@ -11,52 +11,31 @@ import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_event.dart'; import 'package:syncrow_web/services/home_api.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/routes_const.dart'; import 'package:syncrow_web/utils/navigation_service.dart'; class HomeBloc extends Bloc { - // final Graph graph = Graph()..isTree = true; - // final BuchheimWalkerConfiguration builder = BuchheimWalkerConfiguration(); - // List sourcesList = []; - // List destinationsList = []; UserModel? user; String terms = ''; String policy = ''; HomeBloc() : super((HomeInitial())) { - // on(_createNode); on(_fetchUserInfo); on(_fetchTerms); on(_fetchPolicy); on(_confirmUserAgreement); } - // void _createNode(CreateNewNode event, Emitter emit) async { - // emit(HomeInitial()); - // sourcesList.add(event.sourceNode); - // destinationsList.add(event.destinationNode); - // for (int i = 0; i < sourcesList.length; i++) { - // graph.addEdge(sourcesList[i], destinationsList[i]); - // } - - // builder - // ..siblingSeparation = (100) - // ..levelSeparation = (150) - // ..subtreeSeparation = (150) - // ..orientation = (BuchheimWalkerConfiguration.ORIENTATION_TOP_BOTTOM); - // emit(HomeUpdateTree(graph: graph, builder: builder)); - // } - Future _fetchUserInfo(FetchUserInfo event, Emitter emit) async { try { - var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); + var uuid = + await const FlutterSecureStorage().read(key: UserModel.userUuidKey); user = await HomeApi().fetchUserInfo(uuid); if (user != null && user!.project != null) { await ProjectManager.setProjectUUID(user!.project!.uuid); - NavigationService.navigatorKey.currentContext!.read().add(InitialEvent()); + } add(FetchTermEvent()); add(FetchPolicyEvent()); @@ -88,10 +67,12 @@ class HomeBloc extends Bloc { } } - Future _confirmUserAgreement(ConfirmUserAgreementEvent event, Emitter emit) async { + Future _confirmUserAgreement( + ConfirmUserAgreementEvent event, Emitter emit) async { try { emit(LoadingHome()); - var uuid = await const FlutterSecureStorage().read(key: UserModel.userUuidKey); + var uuid = + await const FlutterSecureStorage().read(key: UserModel.userUuidKey); policy = await HomeApi().confirmUserAgreements(uuid); emit(PolicyAgreement()); } catch (e) { @@ -99,16 +80,6 @@ class HomeBloc extends Bloc { } } -// static Future fetchUserInfo() async { -// try { -// var uuid = -// await const FlutterSecureStorage().read(key: UserModel.userUuidKey); -// user = await HomeApi().fetchUserInfo(uuid); -// } catch (e) { -// return; -// } -// } - List homeItems = [ HomeItemModel( title: 'Access Management', @@ -118,7 +89,7 @@ class HomeBloc extends Bloc { context.read().add(ClearCachedData()); context.go(RoutesConst.accessManagementPage); }, - color: null, + color: const Color(0xFF0036E6), ), HomeItemModel( title: 'Space Management', @@ -128,7 +99,7 @@ class HomeBloc extends Bloc { context.read().add(ClearCachedData()); context.go(RoutesConst.spacesManagementPage); }, - color: ColorsManager.primaryColor, + color: const Color(0xFF0026A2), ), HomeItemModel( title: 'Devices Management', @@ -140,43 +111,19 @@ class HomeBloc extends Bloc { .add(const TriggerSwitchTabsEvent(isRoutineTab: false)); context.go(RoutesConst.deviceManagementPage); }, - color: ColorsManager.primaryColor, + color: const Color(0xFF00165E), + ), + HomeItemModel( + title: 'Syncrow Analytics', + icon: Assets.analyticsIcon, + active: true, + onPress: (context) { + context.read().add(ClearCachedData()); + BlocProvider.of(context) + .add(const TriggerSwitchTabsEvent(isRoutineTab: false)); + context.go(RoutesConst.analytics); + }, + color: const Color(0xFF023DFE), ), - - // HomeItemModel( - // title: 'Move in', - // icon: Assets.moveinIcon, - // active: false, - // onPress: (context) {}, - // color: ColorsManager.primaryColor, - // ), - // HomeItemModel( - // title: 'Construction', - // icon: Assets.constructionIcon, - // active: false, - // onPress: (context) {}, - // color: ColorsManager.primaryColor, - // ), - // HomeItemModel( - // title: 'Energy', - // icon: Assets.energyIcon, - // active: false, - // onPress: (context) {}, - // color: ColorsManager.slidingBlueColor.withOpacity(0.2), - // ), - // HomeItemModel( - // title: 'Integrations', - // icon: Assets.integrationsIcon, - // active: false, - // onPress: (context) {}, - // color: ColorsManager.slidingBlueColor.withOpacity(0.2), - // ), - // HomeItemModel( - // title: 'Asset', - // icon: Assets.assetIcon, - // active: false, - // onPress: (context) {}, - // color: ColorsManager.slidingBlueColor.withOpacity(0.2), - // ), ]; } diff --git a/lib/pages/home/view/home_card.dart b/lib/pages/home/view/home_card.dart index d2e71608..4fbe1aa8 100644 --- a/lib/pages/home/view/home_card.dart +++ b/lib/pages/home/view/home_card.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/utils/color_manager.dart'; class HomeCard extends StatelessWidget { final bool active; @@ -8,6 +7,7 @@ class HomeCard extends StatelessWidget { final int index; final String name; final Function()? onTap; + final Color? color; const HomeCard({ super.key, required this.name, @@ -15,28 +15,16 @@ class HomeCard extends StatelessWidget { this.active = false, required this.img, required this.onTap, + required this.color, }); @override Widget build(BuildContext context) { - // bool evenNumbers = index % 2 == 0; return InkWell( onTap: active ? onTap : null, child: Container( padding: const EdgeInsets.only(left: 10, right: 10, bottom: 10), decoration: BoxDecoration( - color: index == 0 && active - ? ColorsManager.blue1.withOpacity(0.9) - : index == 1 && active - ? ColorsManager.blue2.withOpacity(0.9) - : index == 2 && active - ? ColorsManager.blue3 - : index == 4 && active == false - ? ColorsManager.blue4.withOpacity(0.2) - : index == 7 && active == false - ? ColorsManager.blue4.withOpacity(0.2) - : ColorsManager.blueColor.withOpacity(0.2), - // (active ?ColorsManager.blueColor - // : ColorsManager.blueColor.withOpacity(0.2)), + color: color, borderRadius: BorderRadius.circular(30), ), child: Column( @@ -52,7 +40,7 @@ class HomeCard extends StatelessWidget { child: Text( name, style: const TextStyle( - fontSize: 20, + fontSize: 30, color: Colors.white, fontWeight: FontWeight.bold, ), @@ -64,15 +52,9 @@ class HomeCard extends StatelessWidget { ), const SizedBox(height: 10), Expanded( - child: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - SizedBox( - child: SvgPicture.asset( - img, - ), - ), - ], + child: Align( + alignment: AlignmentDirectional.bottomEnd, + child: SvgPicture.asset(img), ), ), ], diff --git a/lib/pages/home/view/home_page_mobile.dart b/lib/pages/home/view/home_page_mobile.dart index d0719c3e..ad019ea8 100644 --- a/lib/pages/home/view/home_page_mobile.dart +++ b/lib/pages/home/view/home_page_mobile.dart @@ -50,7 +50,7 @@ class HomeMobilePage extends StatelessWidget { height: size.height * 0.6, width: size.width * 0.68, child: GridView.builder( - itemCount: homeItems.length, + itemCount: homeBloc.homeItems.length, gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 2, @@ -61,7 +61,8 @@ class HomeMobilePage extends StatelessWidget { itemBuilder: (context, index) { return HomeCard( index: index, - active: homeBloc.homeItems[index].active!, + active: true, + color: homeBloc.homeItems[index].color, name: homeBloc.homeItems[index].title!, img: homeBloc.homeItems[index].icon!, onTap: () => @@ -78,56 +79,4 @@ class HomeMobilePage extends StatelessWidget { ), ); } - - final dynamic homeItems = [ - { - 'title': 'Access', - 'icon': Assets.accessIcon, - 'active': true, - }, - { - 'title': 'Space\nManagement', - 'icon': Assets.spaseManagementIcon, - 'color': ColorsManager.primaryColor, - 'active': true, - }, - { - 'title': 'Devices', - 'icon': Assets.devicesIcon, - 'active': true, - }, - { - 'title': 'Syncrow Analytics', - 'icon': Assets.iconEdit, - 'active': true, - }, - // { - // 'title': 'Move in', - // 'icon': Assets.moveinIcon, - // 'active': false, - // }, - // { - // 'title': 'Construction', - // 'icon': Assets.constructionIcon, - // 'active': false, - // }, - // { - // 'title': 'Energy', - // 'icon': Assets.energyIcon, - // 'color': ColorsManager.slidingBlueColor.withOpacity(0.2), - // 'active': false, - // }, - // { - // 'title': 'Integrations', - // 'icon': Assets.integrationsIcon, - // 'color': ColorsManager.slidingBlueColor.withOpacity(0.2), - // 'active': false, - // }, - // { - // 'title': 'Asset', - // 'icon': Assets.assetIcon, - // 'color': ColorsManager.slidingBlueColor.withOpacity(0.2), - // 'active': false, - // }, - ]; } diff --git a/lib/pages/home/view/home_page_web.dart b/lib/pages/home/view/home_page_web.dart index 9a59f51c..1be60c39 100644 --- a/lib/pages/home/view/home_page_web.dart +++ b/lib/pages/home/view/home_page_web.dart @@ -20,12 +20,7 @@ class _HomeWebPageState extends State { // Flag to track whether the dialog is already shown. bool _dialogShown = false; - @override - void initState() { - super.initState(); - final homeBloc = BlocProvider.of(context); - homeBloc.add(const FetchUserInfo()); - } + @override Widget build(BuildContext context) { @@ -38,8 +33,10 @@ class _HomeWebPageState extends State { child: BlocConsumer( listener: (BuildContext context, state) { if (state is HomeInitial) { - if (homeBloc.user!.hasAcceptedWebAgreement == false && !_dialogShown) { - _dialogShown = true; // Set the flag to true to indicate the dialog is showing. + if (homeBloc.user!.hasAcceptedWebAgreement == false && + !_dialogShown) { + _dialogShown = + true; // Set the flag to true to indicate the dialog is showing. Future.delayed(const Duration(seconds: 1), () { showDialog( context: context, @@ -54,7 +51,7 @@ class _HomeWebPageState extends State { _dialogShown = false; if (v != null) { homeBloc.add(ConfirmUserAgreementEvent()); - homeBloc.add(const FetchUserInfo()); + // homeBloc.add(const FetchUserInfo()); } }); }); @@ -98,19 +95,22 @@ class _HomeWebPageState extends State { width: size.width * 0.68, child: GridView.builder( itemCount: homeBloc.homeItems.length, - gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 3, // Adjust as needed. + gridDelegate: + const SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 4, // Adjust as needed. crossAxisSpacing: 20.0, mainAxisSpacing: 20.0, childAspectRatio: 1.5, ), itemBuilder: (context, index) { return HomeCard( + color: homeBloc.homeItems[index].color, index: index, active: homeBloc.homeItems[index].active!, name: homeBloc.homeItems[index].title!, img: homeBloc.homeItems[index].icon!, - onTap: () => homeBloc.homeItems[index].onPress(context), + onTap: () => + homeBloc.homeItems[index].onPress(context), ); }, ), diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/name_filter.dart b/lib/pages/roles_and_permission/users_page/users_table/view/name_filter.dart index 5ff10b20..f551cf3c 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/name_filter.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/name_filter.dart @@ -5,7 +5,7 @@ import 'package:syncrow_web/utils/constants/assets.dart'; Future showNameMenu({ required BuildContext context, Function()? aToZTap, - Function()? zToaTap, + Function()? zToATap, String? isSelected, }) async { final RenderBox overlay = @@ -46,7 +46,7 @@ Future showNameMenu({ ), ), PopupMenuItem( - onTap: zToaTap, + onTap: zToATap, child: ListTile( leading: Image.asset( Assets.ZtoAIcon, diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/user_table.dart b/lib/pages/roles_and_permission/users_page/users_table/view/user_table.dart index b26c09c4..9b10b5d4 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/user_table.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/user_table.dart @@ -95,7 +95,7 @@ class _TableRow extends StatelessWidget { ], ), if (!isLast) - Divider( + const Divider( height: 1, thickness: 1, color: ColorsManager.boxDivider, @@ -110,12 +110,14 @@ class DynamicTableScreen extends StatefulWidget { final List titles; final List> rows; final void Function(int columnIndex)? onFilter; + final double tableSize; const DynamicTableScreen({ required this.titles, required this.rows, required this.onFilter, Key? key, + required this.tableSize, }) : super(key: key); @override @@ -205,7 +207,8 @@ class _DynamicTableScreenState extends State { bottomRight: Radius.circular(15), ), ), - child: Column( + child: ListView( + shrinkWrap: true, children: [ for (int rowIndex = 0; rowIndex < widget.rows.length; rowIndex++) _TableRow( @@ -253,7 +256,7 @@ class _DynamicTableScreenState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ _buildHeader(), - _buildBody(), + Container(height: widget.tableSize - 37, child: _buildBody()), ], ), ), diff --git a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart index 3c4df20c..767fd9a6 100644 --- a/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart +++ b/lib/pages/roles_and_permission/users_page/users_table/view/users_page.dart @@ -26,7 +26,8 @@ class UsersPage extends StatelessWidget { Widget build(BuildContext context) { final TextEditingController searchController = TextEditingController(); - Widget actionButton({bool isActive = false, required String title, Function()? onTap}) { + Widget actionButton( + {bool isActive = false, required String title, Function()? onTap}) { return InkWell( onTap: onTap, child: Padding( @@ -59,7 +60,8 @@ class UsersPage extends StatelessWidget { : ColorsManager.disabledPink.withOpacity(0.5), ), child: Padding( - padding: const EdgeInsets.only(left: 10, right: 10, bottom: 5, top: 5), + padding: + const EdgeInsets.only(left: 10, right: 10, bottom: 5, top: 5), child: Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center, @@ -83,12 +85,15 @@ class UsersPage extends StatelessWidget { } Widget changeIconStatus( - {required String userId, required String status, required Function()? onTap}) { + {required String userId, + required String status, + required Function()? onTap}) { return Center( child: InkWell( onTap: onTap, child: Padding( - padding: const EdgeInsets.only(left: 5, right: 5, bottom: 5, top: 5), + padding: + const EdgeInsets.only(left: 5, right: 5, bottom: 5, top: 5), child: SvgPicture.asset( status == "invited" ? Assets.invitedIcon @@ -113,8 +118,7 @@ class UsersPage extends StatelessWidget { padding: const EdgeInsets.all(20), child: Align( alignment: Alignment.topCenter, - child: ListView( - shrinkWrap: true, + child: Column( children: [ Row( children: [ @@ -187,292 +191,325 @@ class UsersPage extends StatelessWidget { ), ], ), - const SizedBox(height: 25), - DynamicTableScreen( - onFilter: (columnIndex) { - if (columnIndex == 0) { - showNameMenu( - context: context, - isSelected: _blocRole.currentSortOrder, - aToZTap: () { - context.read().add(const SortUsersByNameAsc()); - }, - zToaTap: () { - context.read().add(const SortUsersByNameDesc()); - }, - ); - } - if (columnIndex == 2) { - final Map checkboxStates = { - for (var item in _blocRole.jobTitle) - item: _blocRole.selectedJobTitles.contains(item), - }; - final RenderBox overlay = - Overlay.of(context).context.findRenderObject() as RenderBox; + const SizedBox(height: 20), + Container( + height: screenSize.height * 0.65, + child: DynamicTableScreen( + tableSize: screenSize.height * 0.65, + onFilter: (columnIndex) { + if (columnIndex == 0) { + showNameMenu( + context: context, + isSelected: _blocRole.currentSortOrder, + aToZTap: () { + context + .read() + .add(const SortUsersByNameAsc()); + }, + zToATap: () { + context + .read() + .add(const SortUsersByNameDesc()); + }, + ); + } + if (columnIndex == 2) { + final Map checkboxStates = { + for (var item in _blocRole.jobTitle) + item: _blocRole.selectedJobTitles.contains(item), + }; + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; - showPopUpFilterMenu( - position: RelativeRect.fromLTRB( - overlay.size.width / 5.3, - 240, - overlay.size.width / 4, - 0, - ), - list: _blocRole.jobTitle, - context: context, - checkboxStates: checkboxStates, - isSelected: _blocRole.currentSortJopTitle, - onOkPressed: () { - searchController.clear(); - _blocRole.add(FilterClearEvent()); - final selectedItems = checkboxStates.entries - .where((entry) => entry.value) - .map((entry) => entry.key) - .toList(); - Navigator.of(context).pop(); - _blocRole.add(FilterUsersByJobEvent( - selectedJob: selectedItems, - sortOrder: _blocRole.currentSortJopTitle, - )); - }, - onSortAtoZ: (v) { - _blocRole.currentSortJopTitle = v; - }, - onSortZtoA: (v) { - _blocRole.currentSortJopTitle = v; - }, - ); - } + showPopUpFilterMenu( + position: RelativeRect.fromLTRB( + overlay.size.width / 5.3, + 240, + overlay.size.width / 4, + 0, + ), + list: _blocRole.jobTitle, + context: context, + checkboxStates: checkboxStates, + isSelected: _blocRole.currentSortJopTitle, + onOkPressed: () { + searchController.clear(); + _blocRole.add(FilterClearEvent()); + final selectedItems = checkboxStates.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + Navigator.of(context).pop(); + _blocRole.add(FilterUsersByJobEvent( + selectedJob: selectedItems, + sortOrder: _blocRole.currentSortJopTitle, + )); + }, + onSortAtoZ: (v) { + _blocRole.currentSortJopTitle = v; + }, + onSortZtoA: (v) { + _blocRole.currentSortJopTitle = v; + }, + ); + } - if (columnIndex == 3) { - final Map checkboxStates = { - for (var item in _blocRole.roleTypes) - item: _blocRole.selectedRoles.contains(item), - }; - final RenderBox overlay = - Overlay.of(context).context.findRenderObject() as RenderBox; - showPopUpFilterMenu( - position: RelativeRect.fromLTRB( - overlay.size.width / 4, - 240, - overlay.size.width / 4, - 0, - ), - list: _blocRole.roleTypes, - context: context, - checkboxStates: checkboxStates, - isSelected: _blocRole.currentSortRole, - onOkPressed: () { - searchController.clear(); - _blocRole.add(FilterClearEvent()); - final selectedItems = checkboxStates.entries - .where((entry) => entry.value) - .map((entry) => entry.key) - .toList(); - Navigator.of(context).pop(); - context.read().add(FilterUsersByRoleEvent( - selectedRoles: selectedItems, - sortOrder: _blocRole.currentSortRole)); - }, - onSortAtoZ: (v) { - _blocRole.currentSortRole = v; - }, - onSortZtoA: (v) { - _blocRole.currentSortRole = v; - }, - ); - } - if (columnIndex == 4) { - showDateFilterMenu( - context: context, - isSelected: _blocRole.currentSortOrder, - aToZTap: () { - context.read().add(const DateNewestToOldestEvent()); - }, - zToaTap: () { - context.read().add(const DateOldestToNewestEvent()); - }, - ); - } - if (columnIndex == 6) { - final Map checkboxStates = { - for (var item in _blocRole.createdBy) - item: _blocRole.selectedCreatedBy.contains(item), - }; - final RenderBox overlay = - Overlay.of(context).context.findRenderObject() as RenderBox; - showPopUpFilterMenu( - position: RelativeRect.fromLTRB( - overlay.size.width / 1, - 240, - overlay.size.width / 4, - 0, - ), - list: _blocRole.createdBy, - context: context, - checkboxStates: checkboxStates, - isSelected: _blocRole.currentSortCreatedBy, - onOkPressed: () { - searchController.clear(); - _blocRole.add(FilterClearEvent()); - final selectedItems = checkboxStates.entries - .where((entry) => entry.value) - .map((entry) => entry.key) - .toList(); - Navigator.of(context).pop(); - _blocRole.add(FilterUsersByCreatedEvent( - selectedCreatedBy: selectedItems, - sortOrder: _blocRole.currentSortCreatedBy)); - }, - onSortAtoZ: (v) { - _blocRole.currentSortCreatedBy = v; - }, - onSortZtoA: (v) { - _blocRole.currentSortCreatedBy = v; - }, - ); - } - if (columnIndex == 7) { - final Map checkboxStates = { - for (var item in _blocRole.status) - item: _blocRole.selectedStatuses.contains(item), - }; + if (columnIndex == 3) { + final Map checkboxStates = { + for (var item in _blocRole.roleTypes) + item: _blocRole.selectedRoles.contains(item), + }; + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + showPopUpFilterMenu( + position: RelativeRect.fromLTRB( + overlay.size.width / 4, + 240, + overlay.size.width / 4, + 0, + ), + list: _blocRole.roleTypes, + context: context, + checkboxStates: checkboxStates, + isSelected: _blocRole.currentSortRole, + onOkPressed: () { + searchController.clear(); + _blocRole.add(FilterClearEvent()); + final selectedItems = checkboxStates.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + Navigator.of(context).pop(); + context.read().add( + FilterUsersByRoleEvent( + selectedRoles: selectedItems, + sortOrder: _blocRole.currentSortRole)); + }, + onSortAtoZ: (v) { + _blocRole.currentSortRole = v; + }, + onSortZtoA: (v) { + _blocRole.currentSortRole = v; + }, + ); + } + if (columnIndex == 4) { + showDateFilterMenu( + context: context, + isSelected: _blocRole.currentSortOrder, + aToZTap: () { + context + .read() + .add(const DateNewestToOldestEvent()); + }, + zToaTap: () { + context + .read() + .add(const DateOldestToNewestEvent()); + }, + ); + } + if (columnIndex == 6) { + final Map checkboxStates = { + for (var item in _blocRole.createdBy) + item: _blocRole.selectedCreatedBy.contains(item), + }; + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + showPopUpFilterMenu( + position: RelativeRect.fromLTRB( + overlay.size.width / 1, + 240, + overlay.size.width / 4, + 0, + ), + list: _blocRole.createdBy, + context: context, + checkboxStates: checkboxStates, + isSelected: _blocRole.currentSortCreatedBy, + onOkPressed: () { + searchController.clear(); + _blocRole.add(FilterClearEvent()); + final selectedItems = checkboxStates.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + Navigator.of(context).pop(); + _blocRole.add(FilterUsersByCreatedEvent( + selectedCreatedBy: selectedItems, + sortOrder: _blocRole.currentSortCreatedBy)); + }, + onSortAtoZ: (v) { + _blocRole.currentSortCreatedBy = v; + }, + onSortZtoA: (v) { + _blocRole.currentSortCreatedBy = v; + }, + ); + } + if (columnIndex == 7) { + final Map checkboxStates = { + for (var item in _blocRole.status) + item: _blocRole.selectedStatuses.contains(item), + }; - final RenderBox overlay = - Overlay.of(context).context.findRenderObject() as RenderBox; - showPopUpFilterMenu( - position: RelativeRect.fromLTRB( - overlay.size.width / 0, - 240, - overlay.size.width / 5, - 0, + final RenderBox overlay = Overlay.of(context) + .context + .findRenderObject() as RenderBox; + showPopUpFilterMenu( + position: RelativeRect.fromLTRB( + overlay.size.width / 0, + 240, + overlay.size.width / 5, + 0, + ), + list: _blocRole.status, + context: context, + checkboxStates: checkboxStates, + isSelected: _blocRole.currentSortStatus, + onOkPressed: () { + searchController.clear(); + _blocRole.add(FilterClearEvent()); + final selectedItems = checkboxStates.entries + .where((entry) => entry.value) + .map((entry) => entry.key) + .toList(); + Navigator.of(context).pop(); + _blocRole.add(FilterUsersByDeActevateEvent( + selectedActivate: selectedItems, + sortOrder: _blocRole.currentSortStatus)); + }, + onSortAtoZ: (v) { + _blocRole.currentSortStatus = v; + }, + onSortZtoA: (v) { + _blocRole.currentSortStatus = v; + }, + ); + } + if (columnIndex == 8) { + showDeActivateFilterMenu( + context: context, + isSelected: _blocRole.currentSortOrderDate, + aToZTap: () { + context + .read() + .add(const DateNewestToOldestEvent()); + }, + zToaTap: () { + context + .read() + .add(const DateOldestToNewestEvent()); + }, + ); + } + }, + titles: const [ + "Full Name", + "Email Address", + "Job Title", + "Role", + "Creation Date", + "Creation Time", + "Created By", + "Status", + "De/Activate", + "Action" + ], + rows: state.users.map((user) { + return [ + Text('${user.firstName} ${user.lastName}'), + Text(user.email), + Text(user.jobTitle), + Text(user.roleType ?? ''), + Text(user.createdDate ?? ''), + Text(user.createdTime ?? ''), + Text(user.invitedBy), + status( + status: user.isEnabled == false + ? 'disabled' + : user.status, ), - list: _blocRole.status, - context: context, - checkboxStates: checkboxStates, - isSelected: _blocRole.currentSortStatus, - onOkPressed: () { - searchController.clear(); - _blocRole.add(FilterClearEvent()); - final selectedItems = checkboxStates.entries - .where((entry) => entry.value) - .map((entry) => entry.key) - .toList(); - Navigator.of(context).pop(); - _blocRole.add(FilterUsersByDeActevateEvent( - selectedActivate: selectedItems, - sortOrder: _blocRole.currentSortStatus)); - }, - onSortAtoZ: (v) { - _blocRole.currentSortStatus = v; - }, - onSortZtoA: (v) { - _blocRole.currentSortStatus = v; - }, - ); - } - if (columnIndex == 8) { - showDeActivateFilterMenu( - context: context, - isSelected: _blocRole.currentSortOrderDate, - aToZTap: () { - context.read().add(const DateNewestToOldestEvent()); - }, - zToaTap: () { - context.read().add(const DateOldestToNewestEvent()); - }, - ); - } - }, - titles: const [ - "Full Name", - "Email Address", - "Job Title", - "Role", - "Creation Date", - "Creation Time", - "Created By", - "Status", - "De/Activate", - "Action" - ], - rows: state.users.map((user) { - return [ - Text('${user.firstName} ${user.lastName}'), - Text(user.email), - Text(user.jobTitle), - Text(user.roleType ?? ''), - Text(user.createdDate ?? ''), - Text(user.createdTime ?? ''), - Text(user.invitedBy), - status( - status: user.isEnabled == false ? 'disabled' : user.status, - ), - changeIconStatus( - status: user.isEnabled == false ? 'disabled' : user.status, - userId: user.uuid, - onTap: user.status != "invited" - ? () { - context.read().add(ChangeUserStatus( - userId: user.uuid, - newStatus: - user.isEnabled == false ? 'disabled' : user.status)); - } - : null, - ), - Row( - children: [ - user.isEnabled != false - ? actionButton( - isActive: true, - title: "Edit", - onTap: () { - context.read().add(ClearCachedData()); - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return EditUserDialog(userId: user.uuid); - }, - ).then((v) { - if (v != null) { + changeIconStatus( + status: user.isEnabled == false + ? 'disabled' + : user.status, + userId: user.uuid, + onTap: user.status != "invited" + ? () { + context.read().add( + ChangeUserStatus( + userId: user.uuid, + newStatus: user.isEnabled == false + ? 'disabled' + : user.status)); + } + : null, + ), + Row( + children: [ + user.isEnabled != false + ? actionButton( + isActive: true, + title: "Edit", + onTap: () { + context + .read() + .add(ClearCachedData()); + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return EditUserDialog( + userId: user.uuid); + }, + ).then((v) { if (v != null) { - _blocRole.add(const GetUsers()); + if (v != null) { + _blocRole.add(const GetUsers()); + } } + }); + }, + ) + : actionButton( + title: "Edit", + ), + actionButton( + title: "Delete", + onTap: () { + showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return DeleteUserDialog( + onTapDelete: () async { + try { + _blocRole.add(DeleteUserEvent( + user.uuid, context)); + await Future.delayed( + const Duration(seconds: 2)); + return true; + } catch (e) { + return false; } }); }, - ) - : actionButton( - title: "Edit", - ), - actionButton( - title: "Delete", - onTap: () { - showDialog( - context: context, - barrierDismissible: false, - builder: (BuildContext context) { - return DeleteUserDialog(onTapDelete: () async { - try { - _blocRole.add(DeleteUserEvent(user.uuid, context)); - await Future.delayed(const Duration(seconds: 2)); - return true; - } catch (e) { - return false; - } - }); - }, - ).then((v) { - if (v != null) { - _blocRole.add(const GetUsers()); - } - }); - }, - ), - ], - ), - ]; - }).toList(), + ).then((v) { + if (v != null) { + _blocRole.add(const GetUsers()); + } + }); + }, + ), + ], + ), + ]; + }).toList(), + ), ), Padding( padding: const EdgeInsets.all(8.0), @@ -485,14 +522,20 @@ class UsersPage extends StatelessWidget { visiblePagesCount: 4, buttonRadius: 10, selectedButtonColor: ColorsManager.secondaryColor, - buttonUnSelectedBorderColor: ColorsManager.grayBorder, - lastPageIcon: const Icon(Icons.keyboard_double_arrow_right), - firstPageIcon: const Icon(Icons.keyboard_double_arrow_left), - totalPages: - (_blocRole.totalUsersCount.length / _blocRole.itemsPerPage).ceil(), + buttonUnSelectedBorderColor: + ColorsManager.grayBorder, + lastPageIcon: + const Icon(Icons.keyboard_double_arrow_right), + firstPageIcon: + const Icon(Icons.keyboard_double_arrow_left), + totalPages: (_blocRole.totalUsersCount.length / + _blocRole.itemsPerPage) + .ceil(), currentPage: _blocRole.currentPage, onPageChanged: (int pageNumber) { - context.read().add(ChangePage(pageNumber)); + context + .read() + .add(ChangePage(pageNumber)); }, ), ), diff --git a/lib/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart b/lib/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart index b472d034..84610b56 100644 --- a/lib/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart +++ b/lib/pages/routines/bloc/create_routine_bloc/create_routine_bloc.dart @@ -11,7 +11,6 @@ class CreateRoutineBloc extends Bloc { on(_fetchSpaceOnlyWithDevices); on(saveSpaceIdCommunityId); on(resetSelected); - on(_fetchCommunity); } String selectedSpaceId = ''; @@ -50,18 +49,4 @@ class CreateRoutineBloc extends Bloc { selectedCommunityId = ''; emit(const ResetSelectedState()); } - - Future _fetchCommunity( - FetchCommunityEvent event, Emitter emit) async { - emit(const CommunitiesLoadingState()); - - try { - final projectUuid = await ProjectManager.getProjectUUID() ?? ''; - communities = - await CommunitySpaceManagementApi().fetchCommunities(projectUuid); - emit(const CommunityLoadedState()); - } catch (e) { - emit(SpaceTreeErrorState('Error loading communities $e')); - } - } } diff --git a/lib/pages/routines/bloc/create_routine_bloc/create_routine_event.dart b/lib/pages/routines/bloc/create_routine_bloc/create_routine_event.dart index ba901497..14c63344 100644 --- a/lib/pages/routines/bloc/create_routine_bloc/create_routine_event.dart +++ b/lib/pages/routines/bloc/create_routine_bloc/create_routine_event.dart @@ -43,9 +43,3 @@ class ResetSelectedEvent extends CreateRoutineEvent { } -class FetchCommunityEvent extends CreateRoutineEvent { - const FetchCommunityEvent(); - - @override - List get props => []; -} \ No newline at end of file diff --git a/lib/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart b/lib/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart index bb4a7a1e..659d3261 100644 --- a/lib/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart +++ b/lib/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart @@ -26,8 +26,10 @@ class FunctionBloc extends Bloc { functionCode: event.functionData.functionCode, operationName: event.functionData.operationName, value: event.functionData.value ?? existingData.value, - valueDescription: event.functionData.valueDescription ?? existingData.valueDescription, + valueDescription: event.functionData.valueDescription ?? + existingData.valueDescription, condition: event.functionData.condition ?? existingData.condition, + step: event.functionData.step ?? existingData.step, ); } else { functions.clear(); @@ -59,8 +61,10 @@ class FunctionBloc extends Bloc { ); } - FutureOr _onSelectFunction(SelectFunction event, Emitter emit) { + FutureOr _onSelectFunction( + SelectFunction event, Emitter emit) { emit(state.copyWith( - selectedFunction: event.functionCode, selectedOperationName: event.operationName)); + selectedFunction: event.functionCode, + selectedOperationName: event.operationName)); } } diff --git a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart index c36227cb..dd73183a 100644 --- a/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart +++ b/lib/pages/routines/bloc/routine_bloc/routine_bloc.dart @@ -1,5 +1,7 @@ import 'dart:async'; +import 'package:bloc/bloc.dart'; +import 'package:dio/dio.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -14,6 +16,7 @@ import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/models/routine_details_model.dart'; import 'package:syncrow_web/pages/routines/models/routine_model.dart'; import 'package:syncrow_web/pages/space_tree/bloc/space_tree_bloc.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/devices_mang_api.dart'; import 'package:syncrow_web/services/routines_api.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; @@ -24,9 +27,6 @@ import 'package:uuid/uuid.dart'; part 'routine_event.dart'; part 'routine_state.dart'; -// String spaceId = '25c96044-fadf-44bb-93c7-3c079e527ce6'; -// String communityId = 'aff21a57-2f91-4e5c-b99b-0182c3ab65a9'; - class RoutineBloc extends Bloc { RoutineBloc() : super(const RoutineState()) { on(_onAddToIfContainer); @@ -63,7 +63,8 @@ class RoutineBloc extends Bloc { TriggerSwitchTabsEvent event, Emitter emit, ) { - emit(state.copyWith(routineTab: event.isRoutineTab, createRoutineView: false)); + emit(state.copyWith( + routineTab: event.isRoutineTab, createRoutineView: false)); add(ResetRoutineState()); if (event.isRoutineTab) { add(const LoadScenes()); @@ -89,8 +90,8 @@ class RoutineBloc extends Bloc { final updatedIfItems = List>.from(state.ifItems); // Find the index of the item in teh current itemsList - int index = updatedIfItems - .indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); + int index = updatedIfItems.indexWhere( + (map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); // Replace the map if the index is valid if (index != -1) { updatedIfItems[index] = event.item; @@ -107,12 +108,13 @@ class RoutineBloc extends Bloc { } } - void _onAddToThenContainer(AddToThenContainer event, Emitter emit) { + void _onAddToThenContainer( + AddToThenContainer event, Emitter emit) { final currentItems = List>.from(state.thenItems); // Find the index of the item in teh current itemsList - int index = currentItems - .indexWhere((map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); + int index = currentItems.indexWhere( + (map) => map['uniqueCustomId'] == event.item['uniqueCustomId']); // Replace the map if the index is valid if (index != -1) { currentItems[index] = event.item; @@ -159,7 +161,8 @@ class RoutineBloc extends Bloc { // currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions); // } - currentSelectedFunctions[event.uniqueCustomId] = List.from(event.functions); + currentSelectedFunctions[event.uniqueCustomId] = + List.from(event.functions); emit(state.copyWith(selectedFunctions: currentSelectedFunctions)); } catch (e) { @@ -167,42 +170,45 @@ class RoutineBloc extends Bloc { } } - Future _onLoadScenes(LoadScenes event, Emitter emit) async { - emit(state.copyWith(isLoading: true, errorMessage: null)); - List scenes = []; - try { - BuildContext context = NavigationService.navigatorKey.currentContext!; - var createRoutineBloc = context.read(); - final projectUuid = await ProjectManager.getProjectUUID() ?? ''; - if (createRoutineBloc.selectedSpaceId == '' && - createRoutineBloc.selectedCommunityId == '') { - var spaceBloc = context.read(); - for (var communityId in spaceBloc.state.selectedCommunities) { - List spacesList = - spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; - for (var spaceId in spacesList) { - scenes - .addAll(await SceneApi.getScenes(spaceId, communityId, projectUuid)); - } +Future _onLoadScenes( + LoadScenes event, Emitter emit) async { + emit(state.copyWith(isLoading: true, errorMessage: null)); + List scenes = []; + try { + BuildContext context = NavigationService.navigatorKey.currentContext!; + var createRoutineBloc = context.read(); + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + if (createRoutineBloc.selectedSpaceId == '' && + createRoutineBloc.selectedCommunityId == '') { + var spaceBloc = context.read(); + for (var communityId in spaceBloc.state.selectedCommunities) { + List spacesList = + spaceBloc.state.selectedCommunityAndSpaces[communityId] ?? []; + for (var spaceId in spacesList) { + scenes.addAll( + await SceneApi.getScenes(spaceId, communityId, projectUuid)); } - } else { - scenes.addAll(await SceneApi.getScenes(createRoutineBloc.selectedSpaceId, - createRoutineBloc.selectedCommunityId, projectUuid)); } - - emit(state.copyWith( - scenes: scenes, - isLoading: false, - )); - } catch (e) { - emit(state.copyWith( - isLoading: false, - loadScenesErrorMessage: 'Failed to load scenes', - errorMessage: '', - loadAutomationErrorMessage: '', - scenes: scenes)); + } else { + scenes.addAll(await SceneApi.getScenes( + createRoutineBloc.selectedSpaceId, + createRoutineBloc.selectedCommunityId, + projectUuid)); } + + emit(state.copyWith( + scenes: scenes, + isLoading: false, + )); + } catch (e) { + emit(state.copyWith( + isLoading: false, + loadScenesErrorMessage: 'Failed to load scenes', + errorMessage: '', + loadAutomationErrorMessage: '', + scenes: scenes)); } +} Future _onLoadAutomation( LoadAutomation event, Emitter emit) async { @@ -281,7 +287,8 @@ class RoutineBloc extends Bloc { if (_isLastActionDelay(state.thenItems)) { emit(state.copyWith( - errorMessage: 'A delay condition cannot be the only or the last action', + errorMessage: + 'A delay condition cannot be the only or the last action', isLoading: false, )); return; @@ -349,10 +356,12 @@ class RoutineBloc extends Bloc { errorMessage: 'Something went wrong', )); } - } catch (e) { + } on APIException catch (e) { + final errorData = e.message; + String errorMessage = errorData; emit(state.copyWith( isLoading: false, - errorMessage: 'Something went wrong', + errorMessage: errorMessage, )); } } @@ -380,7 +389,8 @@ class RoutineBloc extends Bloc { if (_isLastActionDelay(state.thenItems)) { emit(state.copyWith( - errorMessage: 'A delay condition cannot be the only or the last action', + errorMessage: + 'A delay condition cannot be the only or the last action', isLoading: false, )); CustomSnackBar.redSnackBar('Cannot have delay as the last action'); @@ -484,12 +494,14 @@ class RoutineBloc extends Bloc { )); CustomSnackBar.redSnackBar('Something went wrong'); } - } catch (e) { + } on APIException catch (e) { + final errorData = e.message; + String errorMessage = errorData; emit(state.copyWith( isLoading: false, - errorMessage: 'Something went wrong', + errorMessage: errorMessage, )); - CustomSnackBar.redSnackBar('Something went wrong'); + CustomSnackBar.redSnackBar(errorMessage); } } @@ -518,7 +530,8 @@ class RoutineBloc extends Bloc { isAutomation: false, isTabToRun: false)); } else { - emit(state.copyWith(ifItems: ifItems, selectedFunctions: selectedFunctions)); + emit(state.copyWith( + ifItems: ifItems, selectedFunctions: selectedFunctions)); } } } @@ -558,7 +571,7 @@ class RoutineBloc extends Bloc { // 'entityId': 'tab_to_run', // 'uniqueCustomId': const Uuid().v4(), // 'deviceId': 'tab_to_run', - // 'title': 'Tab to run', + // 'title': 'Tap to run', // 'productType': 'tab_to_run', // 'imagePath': Assets.tabToRun, // } @@ -712,7 +725,8 @@ class RoutineBloc extends Bloc { // if (!deviceCards.containsKey(deviceId)) { deviceCards[deviceId] = { 'entityId': action.entityId, - 'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId, + 'deviceId': + action.actionExecutor == 'delay' ? 'delay' : action.entityId, 'uniqueCustomId': action.type == 'automation' || action.actionExecutor == 'delay' ? action.entityId @@ -796,7 +810,7 @@ class RoutineBloc extends Bloc { 'entityId': 'tab_to_run', 'uniqueCustomId': const Uuid().v4(), 'deviceId': 'tab_to_run', - 'title': 'Tab to run', + 'title': 'Tap to run', 'productType': 'tab_to_run', 'imagePath': Assets.tabToRun, } @@ -848,7 +862,8 @@ class RoutineBloc extends Bloc { createRoutineView: false)); } - FutureOr _deleteScene(DeleteScene event, Emitter emit) async { + FutureOr _deleteScene( + DeleteScene event, Emitter emit) async { try { final projectId = await ProjectManager.getProjectUUID() ?? ''; @@ -881,11 +896,14 @@ class RoutineBloc extends Bloc { add(const LoadAutomation()); add(ResetRoutineState()); emit(state.copyWith(isLoading: false, createRoutineView: false)); - } catch (e) { + } on APIException catch (e) { + final errorData = e.message; + String errorMessage = errorData; emit(state.copyWith( isLoading: false, - errorMessage: 'Failed to delete scene', + errorMessage: errorMessage, )); + CustomSnackBar.redSnackBar(errorMessage); } } @@ -951,7 +969,8 @@ class RoutineBloc extends Bloc { if (_isLastActionDelay(state.thenItems)) { emit(state.copyWith( - errorMessage: 'A delay condition cannot be the only or the last action', + errorMessage: + 'A delay condition cannot be the only or the last action', isLoading: false, )); return; @@ -1149,10 +1168,11 @@ class RoutineBloc extends Bloc { errorMessage: result['message'], )); } - } catch (e) { + } on APIException catch (e) { + final errorData = e.message; emit(state.copyWith( isLoading: false, - errorMessage: 'Something went wrong', + errorMessage: errorData, )); } } @@ -1249,7 +1269,8 @@ class RoutineBloc extends Bloc { // if (!deviceThenCards.containsKey(deviceId)) { deviceThenCards[deviceId] = { 'entityId': action.entityId, - 'deviceId': action.actionExecutor == 'delay' ? 'delay' : action.entityId, + 'deviceId': + action.actionExecutor == 'delay' ? 'delay' : action.entityId, 'uniqueCustomId': const Uuid().v4(), 'title': action.actionExecutor == 'delay' ? 'Delay' @@ -1284,7 +1305,8 @@ class RoutineBloc extends Bloc { updatedFunctions[uniqueCustomId] = []; } - if (action.executorProperty != null && action.actionExecutor != 'delay') { + if (action.executorProperty != null && + action.actionExecutor != 'delay') { final functions = matchingDevice.functions; final functionCode = action.executorProperty!.functionCode; for (var function in functions) { @@ -1326,8 +1348,9 @@ class RoutineBloc extends Bloc { } } - final ifItems = - deviceIfCards.values.where((card) => card['type'] == 'condition').toList(); + final ifItems = deviceIfCards.values + .where((card) => card['type'] == 'condition') + .toList(); final thenItems = deviceThenCards.values .where((card) => card['type'] == 'action' || @@ -1397,7 +1420,9 @@ class RoutineBloc extends Bloc { if (success) { final updatedAutomations = await SceneApi.getAutomationByUnitId( - event.automationStatusUpdate.spaceUuid, event.communityId, projectId); + event.automationStatusUpdate.spaceUuid, + event.communityId, + projectId); // Remove from loading set safely final updatedLoadingIds = {...state.loadingAutomationIds!} diff --git a/lib/pages/routines/create_new_routines/commu_dropdown.dart b/lib/pages/routines/create_new_routines/commu_dropdown.dart index 5b96e977..9f5cc33a 100644 --- a/lib/pages/routines/create_new_routines/commu_dropdown.dart +++ b/lib/pages/routines/create_new_routines/commu_dropdown.dart @@ -1,29 +1,66 @@ -import 'package:dropdown_button2/dropdown_button2.dart'; import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/routines/create_new_routines/dropdown_menu_content.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/utils/color_manager.dart'; +import 'space_tree_dropdown_bloc.dart'; -class CommunityDropdown extends StatelessWidget { - final String? selectedValue; - final List communities; - final Function(String?) onChanged; - final TextEditingController _searchController = TextEditingController(); +class SpaceTreeDropdown extends StatelessWidget { + final String? selectedSpaceId; + final Function(String?)? onChanged; - CommunityDropdown({ - Key? key, - required this.selectedValue, - required this.onChanged, - required this.communities, - }) : super(key: key); + const SpaceTreeDropdown({ + super.key, + this.selectedSpaceId, + this.onChanged, + }); @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.all(10.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( + return BlocProvider( + create: (context) { + final bloc = SpaceTreeDropdownBloc(selectedSpaceId); + bloc.add(FetchSpacesEvent()); + return bloc; + }, + child: _DropdownContent(onChanged: onChanged), + ); + } +} + +class _DropdownContent extends StatefulWidget { + final Function(String?)? onChanged; + + const _DropdownContent({this.onChanged}); + + @override + State<_DropdownContent> createState() => _DropdownContentState(); +} + +class _DropdownContentState extends State<_DropdownContent> { + final LayerLink _layerLink = LayerLink(); + OverlayEntry? _overlayEntry; + + @override + void dispose() { + _removeOverlay(); + super.dispose(); + } + + void _removeOverlay() { + _overlayEntry?.remove(); + _overlayEntry = null; + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( "Community", style: Theme.of(context).textTheme.bodyMedium!.copyWith( fontWeight: FontWeight.w400, @@ -31,126 +68,149 @@ class CommunityDropdown extends StatelessWidget { color: ColorsManager.blackColor, ), ), - const SizedBox(height: 8), - SizedBox( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - ), - child: DropdownButton2( - underline: const SizedBox(), - value: selectedValue, - items: communities.map((community) { - return DropdownMenuItem( - value: community.uuid, - child: Text( - ' ${community.name}', - overflow: TextOverflow.ellipsis, - maxLines: 1, - ), - ); - }).toList(), - onChanged: onChanged, - style: const TextStyle(color: Colors.black), - hint: Padding( - padding: const EdgeInsets.only(left: 10), - child: Text( - " Please Select", - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: ColorsManager.textGray, - ), - ), - ), - customButton: Container( - height: 45, - decoration: BoxDecoration( - border: Border.all(color: ColorsManager.textGray, width: 1.0), - borderRadius: BorderRadius.circular(10), - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - flex: 5, - child: Text( - selectedValue != null - ? " ${communities.firstWhere((element) => element.uuid == selectedValue).name}" - : ' Please Select', - style: Theme.of(context).textTheme.bodySmall!.copyWith( - color: selectedValue != null - ? Colors.black - : ColorsManager.textGray, - ), - overflow: TextOverflow.ellipsis, - ), - ), - Expanded( - child: Container( - decoration: BoxDecoration( - color: Colors.grey[100], - borderRadius: const BorderRadius.only( - topRight: Radius.circular(10), - bottomRight: Radius.circular(10), - ), - ), - height: 45, - child: const Icon( - Icons.keyboard_arrow_down, - color: ColorsManager.textGray, - ), - ), - ), - ], - ), - ), - dropdownStyleData: DropdownStyleData( - maxHeight: MediaQuery.of(context).size.height * 0.4, - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(10), - ), - ), - dropdownSearchData: DropdownSearchData( - searchController: _searchController, - searchInnerWidgetHeight: 50, - searchInnerWidget: Container( - height: 50, - padding: - const EdgeInsets.symmetric(horizontal: 8, vertical: 4), - child: TextFormField( - style: const TextStyle(color: Colors.black), - controller: _searchController, - decoration: InputDecoration( - isDense: true, - contentPadding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 12, - ), - hintText: 'Search for community...', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(8), - ), - ), - ), - ), - searchMatchFn: (item, searchValue) { - final communityName = - (item.child as Text).data?.toLowerCase() ?? ''; - return communityName - .contains(searchValue.toLowerCase().trim()); - }, - ), - onMenuStateChange: (isOpen) { - if (!isOpen) { - _searchController.clear(); - } + ), + CompositedTransformTarget( + link: _layerLink, + child: GestureDetector( + onTap: () => _toggleDropdown(context), + child: BlocBuilder( + builder: (context, state) { + return _buildDropdownTrigger(state); }, - menuItemStyleData: const MenuItemStyleData( - height: 40, + ), + ), + ), + ], + ); + } + + Widget _buildDropdownTrigger(SpaceTreeDropdownState state) { + if (state.status == SpaceTreeDropdownStatus.loading) { + return Container( + height: 46, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + margin: const EdgeInsets.symmetric(horizontal: 10), + child: const Center(child: CircularProgressIndicator()), + ); + } + + if (state.status == SpaceTreeDropdownStatus.failure) { + return Container( + height: 46, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + margin: const EdgeInsets.symmetric(horizontal: 10), + child: Center( + child: Text( + 'Error: ${state.errorMessage}', + style: const TextStyle(color: Colors.red), + ), + ), + ); + } + + final selectedCommunity = _findCommunity(state, state.selectedSpaceId); + + return Container( + height: 46, + decoration: BoxDecoration( + border: Border.all(color: Colors.grey.shade300), + borderRadius: BorderRadius.circular(12), + ), + margin: const EdgeInsets.symmetric(horizontal: 10), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 10), + child: Text( + selectedCommunity?.name ?? 'Please Select', + style: TextStyle( + color: selectedCommunity != null + ? ColorsManager.blackColor + : ColorsManager.textGray, + overflow: TextOverflow.ellipsis, + fontWeight: FontWeight.w400, + fontSize: 13, ), ), - )) + ), + Container( + decoration: BoxDecoration( + color: Colors.grey[200], + borderRadius: const BorderRadius.only( + topRight: Radius.circular(10), + bottomRight: Radius.circular(10), + ), + ), + height: 45, + width: 33, + child: const Icon( + Icons.keyboard_arrow_down, + color: ColorsManager.textGray, + ), + ), ], ), ); } + + void _toggleDropdown(BuildContext context) { + if (_overlayEntry != null) { + _removeOverlay(); + return; + } + + final bloc = context.read(); + + _overlayEntry = OverlayEntry( + builder: (context) => Positioned( + width: 300, + child: CompositedTransformFollower( + link: _layerLink, + showWhenUnlinked: false, + offset: const Offset(0, 48), + child: Material( + color: ColorsManager.whiteColors, + elevation: 8, + borderRadius: BorderRadius.circular(12), + child: BlocProvider.value( + value: bloc, + child: DropdownMenuContent( + selectedSpaceId: bloc.state.selectedSpaceId, + onChanged: (id) { + if (id != null && mounted) { + bloc.add(SpaceTreeDropdownSelectEvent(id)); + widget.onChanged?.call(id); + _removeOverlay(); + } + }, + onClose: _removeOverlay, + ), + ), + ), + ), + ), + ); + + Overlay.of(context).insert(_overlayEntry!); + } + + CommunityModel? _findCommunity( + SpaceTreeDropdownState state, String? communityId) { + if (communityId == null) return null; + try { + return state.filteredCommunities.firstWhere((c) => c.uuid == communityId); + } catch (_) {} + try { + return state.communities.firstWhere((c) => c.uuid == communityId); + } catch (e) { + return null; + } + } } diff --git a/lib/pages/routines/create_new_routines/create_new_routines.dart b/lib/pages/routines/create_new_routines/create_new_routines.dart index 8f28208f..7bc38e09 100644 --- a/lib/pages/routines/create_new_routines/create_new_routines.dart +++ b/lib/pages/routines/create_new_routines/create_new_routines.dart @@ -18,19 +18,19 @@ class CreateNewRoutinesDialog extends StatefulWidget { class _CreateNewRoutinesDialogState extends State { String? _selectedCommunity; String? _selectedSpace; + String? _selectedId; @override Widget build(BuildContext context) { return BlocProvider( - create: (BuildContext context) => - CreateRoutineBloc()..add(const FetchCommunityEvent()), + create: (BuildContext context) => CreateRoutineBloc(), child: BlocBuilder( builder: (context, state) { final _bloc = BlocProvider.of(context); final spaces = _bloc.spacesOnlyWithDevices; final isLoadingCommunities = state is CommunitiesLoadingState; final isLoadingSpaces = state is SpaceWithDeviceLoadingState; - String spaceHint = 'Select a community first'; + String spaceHint = 'Please Select'; if (_selectedCommunity != null) { if (isLoadingSpaces) { spaceHint = 'Loading spaces...'; @@ -40,7 +40,10 @@ class _CreateNewRoutinesDialogState extends State { spaceHint = 'Select Space'; } } - + if (_selectedId != null && _selectedCommunity != _selectedId) { + _selectedSpace = null; + _selectedCommunity = _selectedId; + } return AlertDialog( backgroundColor: Colors.white, insetPadding: EdgeInsets.zero, @@ -51,7 +54,9 @@ class _CreateNewRoutinesDialogState extends State { 'Create New Routines', textAlign: TextAlign.center, style: Theme.of(context).textTheme.bodyMedium!.copyWith( - color: ColorsManager.primaryColor, + color: ColorsManager.spaceColor, + fontSize: 20, + fontWeight: FontWeight.w700, ), ), content: Stack( @@ -60,40 +65,44 @@ class _CreateNewRoutinesDialogState extends State { mainAxisSize: MainAxisSize.min, children: [ const Divider(), - Padding( - padding: const EdgeInsets.only(left: 15, right: 15), - child: CommunityDropdown( - communities: _bloc.communities..sort( - (a, b) => a.name.toLowerCase().compareTo( - b.name.toLowerCase(), + const SizedBox(height: 20), + Column( + children: [ + Padding( + padding: + const EdgeInsets.only(left: 13, right: 8), + child: Column( + children: [ + SpaceTreeDropdown( + selectedSpaceId: _selectedId, + onChanged: (String? newValue) { + setState(() => _selectedId = newValue!); + if (_selectedId != null) { + _bloc.add(SpaceOnlyWithDevicesEvent( + _selectedId!)); + } + }, ), + ], + )), + const SizedBox(height: 5), + const SizedBox(height: 8), + Padding( + padding: const EdgeInsets.only(left: 15, right: 15), + child: SpaceDropdown( + hintMessage: spaceHint, + spaces: spaces, + selectedValue: _selectedSpace, + onChanged: (String? newValue) { + setState(() { + _selectedSpace = newValue; + }); + }, ), - selectedValue: _selectedCommunity, - onChanged: (String? newValue) { - setState(() { - _selectedCommunity = newValue; - _selectedSpace = null; - }); - if (newValue != null) { - _bloc.add(SpaceOnlyWithDevicesEvent(newValue)); - } - }, - ), - ), - const SizedBox(height: 5), - Padding( - padding: const EdgeInsets.only(left: 15, right: 15), - child: SpaceDropdown( - hintMessage: spaceHint, - spaces: spaces, - selectedValue: _selectedSpace, - onChanged: (String? newValue) { - setState(() { - _selectedSpace = newValue; - }); - }, - ), + ), + ], ), + const SizedBox(height: 20), const Divider(), Row( mainAxisAlignment: MainAxisAlignment.spaceAround, diff --git a/lib/pages/routines/create_new_routines/dropdown_menu_content.dart b/lib/pages/routines/create_new_routines/dropdown_menu_content.dart new file mode 100644 index 00000000..65243f53 --- /dev/null +++ b/lib/pages/routines/create_new_routines/dropdown_menu_content.dart @@ -0,0 +1,143 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'space_tree_dropdown_bloc.dart'; + +class DropdownMenuContent extends StatefulWidget { + final String? selectedSpaceId; + final ValueChanged onChanged; + final VoidCallback onClose; + + const DropdownMenuContent({ + super.key, + required this.selectedSpaceId, + required this.onChanged, + required this.onClose, + }); + + @override + State createState() => _DropdownMenuContentState(); +} + +class _DropdownMenuContentState extends State { + final ScrollController _scrollController = ScrollController(); + final TextEditingController _searchController = TextEditingController(); + Timer? _debounceTimer; + + @override + void initState() { + super.initState(); + _scrollController.addListener(_onScroll); + } + + @override + void dispose() { + _debounceTimer?.cancel(); + _scrollController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + void _onScroll() { + final bloc = context.read(); + final state = bloc.state; + if (_scrollController.position.pixels >= + _scrollController.position.maxScrollExtent - 30) { + if (state.paginationModel?.hasNext == true && + !state.paginationIsLoading) { + bloc.add(PaginationEvent()); + } + } + } + + void _handleSearch(String query) { + _debounceTimer?.cancel(); + _debounceTimer = Timer(const Duration(milliseconds: 500), () { + context.read().add(SearchQueryEvent(query)); + }); + } + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: const BoxConstraints(maxHeight: 300), + child: BlocBuilder( + builder: (context, state) { + final communities = state.searchQuery.isNotEmpty + ? state.filteredCommunities + : state.communities; + + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: const EdgeInsets.all(8.0), + child: TextFormField( + controller: _searchController, + onChanged: _handleSearch, + style: const TextStyle(fontSize: 14, color: Colors.black), + decoration: InputDecoration( + hintText: 'Search for space...', + prefixIcon: const Icon(Icons.search, size: 20), + contentPadding: + const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + isDense: true, + ), + ), + ), + Expanded( + child: ListView.builder( + controller: _scrollController, + itemCount: + communities.length + (state.paginationIsLoading ? 1 : 0), + itemBuilder: (context, index) { + if (index >= communities.length) { + return state.paginationIsLoading + ? const Padding( + padding: EdgeInsets.all(8.0), + child: Center( + child: SizedBox( + width: 20, + height: 20, + child: + CircularProgressIndicator(strokeWidth: 2), + ), + ), + ) + : const SizedBox.shrink(); + } + + final community = communities[index]; + final isSelected = community.uuid == widget.selectedSpaceId; + + return ListTile( + title: Text( + community.name, + style: TextStyle( + color: isSelected ? Colors.blue : Colors.black, + fontWeight: + isSelected ? FontWeight.bold : FontWeight.normal, + ), + ), + onTap: () { + context + .read() + .add(SearchQueryEvent('')); + + widget.onChanged(community.uuid); + widget.onClose(); + }, + ); + }, + ), + ), + ], + ); + }, + ), + ); + } +} diff --git a/lib/pages/routines/create_new_routines/space_dropdown.dart b/lib/pages/routines/create_new_routines/space_dropdown.dart index a26ff9f4..1d11b02d 100644 --- a/lib/pages/routines/create_new_routines/space_dropdown.dart +++ b/lib/pages/routines/create_new_routines/space_dropdown.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/utils/color_manager.dart'; - class SpaceDropdown extends StatelessWidget { final List spaces; final String? selectedValue; @@ -21,7 +20,7 @@ class SpaceDropdown extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.all(10.0), + padding: const EdgeInsets.only(left: 10), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -33,7 +32,6 @@ class SpaceDropdown extends StatelessWidget { color: ColorsManager.blackColor, ), ), - const SizedBox(height: 8), SizedBox( child: Container( decoration: BoxDecoration( @@ -90,7 +88,7 @@ class SpaceDropdown extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Expanded( - flex: 5, + flex: 6, child: Padding( padding: const EdgeInsets.only(left: 10), child: Text( diff --git a/lib/pages/routines/create_new_routines/space_tree_dropdown_bloc.dart b/lib/pages/routines/create_new_routines/space_tree_dropdown_bloc.dart new file mode 100644 index 00000000..384f2729 --- /dev/null +++ b/lib/pages/routines/create_new_routines/space_tree_dropdown_bloc.dart @@ -0,0 +1,170 @@ +import 'dart:async'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/common/bloc/project_manager.dart'; +import 'package:syncrow_web/pages/space_tree/model/pagination_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; +import 'package:syncrow_web/services/space_mana_api.dart'; +part 'space_tree_dropdown_event.dart'; +part 'space_tree_dropdown_state.dart'; + +class SpaceTreeDropdownBloc + extends Bloc { + SpaceTreeDropdownBloc(String? initialId) + : super(SpaceTreeDropdownState(selectedSpaceId: initialId)) { + on(_onSelect); + on(_onReset); + on(_fetchSpaces); + on(_onSearch); + on(_onPagination); + on(_onDebouncedSearch); + } + Timer? _debounceTimer; + + void _onSelect( + SpaceTreeDropdownSelectEvent event, + Emitter emit, + ) { + final exists = state.communities.any((c) => c.uuid == event.spaceId); + + if (!exists) { + final community = state.filteredCommunities.firstWhere( + (c) => c.uuid == event.spaceId, + orElse: () => CommunityModel( + uuid: event.spaceId!, + name: 'Loading...', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + spaces: [], + description: ''), + ); + + emit(state.copyWith( + selectedSpaceId: event.spaceId, + communities: [...state.communities, community], + )); + } else { + emit(state.copyWith(selectedSpaceId: event.spaceId)); + } + } + + void _onReset( + SpaceTreeDropdownResetEvent event, + Emitter emit, + ) { + emit(state.copyWith(selectedSpaceId: event.initialId)); + } + + Future _fetchSpaces( + FetchSpacesEvent event, + Emitter emit, + ) async { + if (state.status != SpaceTreeDropdownStatus.initial) return; + emit(state.copyWith(status: SpaceTreeDropdownStatus.loading)); + try { + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + final paginationModel = await CommunitySpaceManagementApi() + .fetchCommunitiesAndSpaces(projectId: projectUuid, page: 1); + + emit(state.copyWith( + status: SpaceTreeDropdownStatus.success, + communities: paginationModel.communities, + filteredCommunities: paginationModel.communities, + paginationModel: paginationModel, + )); + } catch (e) { + emit(state.copyWith( + status: SpaceTreeDropdownStatus.failure, + errorMessage: 'Error loading communities: $e', + )); + } + } + + void _onSearch( + SearchQueryEvent event, + Emitter emit, + ) { + _debounceTimer?.cancel(); + _debounceTimer = Timer(const Duration(seconds: 1), () { + add(DebouncedSearchEvent(event.searchQuery)); + }); + } + + Future _onDebouncedSearch( + DebouncedSearchEvent event, + Emitter emit, + ) async { + emit(state.copyWith(isSearching: true)); + + try { + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + final paginationModel = + await CommunitySpaceManagementApi().fetchCommunitiesAndSpaces( + projectId: projectUuid, + page: 1, + search: event.searchQuery, + ); + + emit(state.copyWith( + filteredCommunities: paginationModel.communities, + isSearching: false, + searchQuery: event.searchQuery, + paginationModel: paginationModel, + )); + } catch (e) { + emit(state.copyWith( + isSearching: false, + errorMessage: 'Error searching communities: $e', + )); + } + } + + @override + Future close() { + _debounceTimer?.cancel(); + return super.close(); + } + + Future _onPagination( + PaginationEvent event, + Emitter emit, + ) async { + if (state.paginationIsLoading || state.paginationModel?.hasNext != true) { + return; + } + + emit(state.copyWith(paginationIsLoading: true)); + + try { + final nextPage = state.paginationModel!.pageNum; + final projectUuid = await ProjectManager.getProjectUUID() ?? ''; + final newPagination = await CommunitySpaceManagementApi() + .fetchCommunitiesAndSpaces(projectId: projectUuid, page: nextPage); + + final combinedCommunities = [ + ...state.communities, + ...newPagination.communities + ]; + List filteredCommunities; + if (state.searchQuery.isNotEmpty) { + final query = state.searchQuery.toLowerCase(); + filteredCommunities = combinedCommunities.where((community) { + return community.name.toLowerCase().contains(query); + }).toList(); + } else { + filteredCommunities = combinedCommunities; + } + + emit(state.copyWith( + communities: combinedCommunities, + filteredCommunities: filteredCommunities, + paginationModel: newPagination, + paginationIsLoading: false, + )); + } catch (e) { + emit(state.copyWith( + paginationIsLoading: false, + errorMessage: 'Error loading more communities: $e', + )); + } + } +} diff --git a/lib/pages/routines/create_new_routines/space_tree_dropdown_event.dart b/lib/pages/routines/create_new_routines/space_tree_dropdown_event.dart new file mode 100644 index 00000000..24047f7a --- /dev/null +++ b/lib/pages/routines/create_new_routines/space_tree_dropdown_event.dart @@ -0,0 +1,31 @@ +part of 'space_tree_dropdown_bloc.dart'; + +abstract class SpaceTreeDropdownEvent {} + +class SpaceTreeDropdownSelectEvent extends SpaceTreeDropdownEvent { + final String? spaceId; + + SpaceTreeDropdownSelectEvent(this.spaceId); +} + +class SpaceTreeDropdownResetEvent extends SpaceTreeDropdownEvent { + final String? initialId; + + SpaceTreeDropdownResetEvent(this.initialId); +} + +class FetchSpacesEvent extends SpaceTreeDropdownEvent {} + +class SearchQueryEvent extends SpaceTreeDropdownEvent { + final String searchQuery; + + SearchQueryEvent(this.searchQuery); +} + +class DebouncedSearchEvent extends SpaceTreeDropdownEvent { + final String searchQuery; + + DebouncedSearchEvent(this.searchQuery); +} + +class PaginationEvent extends SpaceTreeDropdownEvent {} \ No newline at end of file diff --git a/lib/pages/routines/create_new_routines/space_tree_dropdown_state.dart b/lib/pages/routines/create_new_routines/space_tree_dropdown_state.dart new file mode 100644 index 00000000..428aa41d --- /dev/null +++ b/lib/pages/routines/create_new_routines/space_tree_dropdown_state.dart @@ -0,0 +1,51 @@ +part of 'space_tree_dropdown_bloc.dart'; + +enum SpaceTreeDropdownStatus { initial, loading, success, failure } + +class SpaceTreeDropdownState { + final String? selectedSpaceId; + final List communities; + final List filteredCommunities; + final SpaceTreeDropdownStatus status; + final String? errorMessage; + final String searchQuery; + final bool paginationIsLoading; + final PaginationModel? paginationModel; + final bool isSearching; + + SpaceTreeDropdownState({ + this.selectedSpaceId, + this.communities = const [], + this.filteredCommunities = const [], + this.status = SpaceTreeDropdownStatus.initial, + this.errorMessage, + this.searchQuery = '', + this.paginationIsLoading = false, + this.paginationModel, + this.isSearching = false, + }); + + SpaceTreeDropdownState copyWith({ + String? selectedSpaceId, + List? communities, + List? filteredCommunities, + SpaceTreeDropdownStatus? status, + String? errorMessage, + String? searchQuery, + bool? paginationIsLoading, + PaginationModel? paginationModel, + bool? isSearching, + }) { + return SpaceTreeDropdownState( + selectedSpaceId: selectedSpaceId ?? this.selectedSpaceId, + communities: communities ?? this.communities, + filteredCommunities: filteredCommunities ?? this.filteredCommunities, + status: status ?? this.status, + errorMessage: errorMessage ?? this.errorMessage, + searchQuery: searchQuery ?? this.searchQuery, + paginationIsLoading: paginationIsLoading ?? this.paginationIsLoading, + paginationModel: paginationModel ?? this.paginationModel, + isSearching: isSearching ?? this.isSearching, + ); + } +} \ No newline at end of file diff --git a/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart b/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart index a94a312b..013626d8 100644 --- a/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart +++ b/lib/pages/routines/helper/dialog_helper/device_dialog_helper.dart @@ -7,9 +7,11 @@ import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_senso import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/gateway/gateway_helper.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/three_gang_switch_dialog.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/two_gang_switch_dialog.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/wall_presence_sensor.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/water_heater/water_heater_presence_sensor.dart'; class DeviceDialogHelper { static Future?> showDeviceDialog({ @@ -116,6 +118,7 @@ class DeviceDialogHelper { uniqueCustomId: data['uniqueCustomId'], deviceSelectedFunctions: deviceSelectedFunctions, device: data['device'], + dialogType: dialogType, ); case 'NCPS': return FlushPresenceSensor.showFlushFunctionsDialog( @@ -126,6 +129,25 @@ class DeviceDialogHelper { dialogType: dialogType, device: data['device'], ); + case 'WH': + return WaterHeaterDialogRoutines.showWHFunctionsDialog( + context: context, + functions: functions, + uniqueCustomId: data['uniqueCustomId'], + deviceSelectedFunctions: deviceSelectedFunctions, + dialogType: dialogType, + device: data['device'], + ); + + case 'PC': + return EnergyClampDialog.showEnergyClampFunctionsDialog( + context: context, + functions: functions, + uniqueCustomId: data['uniqueCustomId'], + deviceSelectedFunctions: deviceSelectedFunctions, + dialogType: dialogType, + device: data['device'], + ); default: return null; diff --git a/lib/pages/routines/helper/save_routine_helper.dart b/lib/pages/routines/helper/save_routine_helper.dart index 23920ba6..f8b52dab 100644 --- a/lib/pages/routines/helper/save_routine_helper.dart +++ b/lib/pages/routines/helper/save_routine_helper.dart @@ -109,12 +109,12 @@ class SaveRoutineHelper { spacing: 16, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - DialogFooterButton( - text: 'Cancel', + DialogFooterButton( + text: 'Back', onTap: () => Navigator.pop(context), ), DialogFooterButton( - text: 'Confirm', + text: 'Save', onTap: () { if (state.isAutomation) { if (state.isUpdate ?? false) { @@ -162,7 +162,7 @@ class SaveRoutineHelper { width: 24, height: 24, ), - title: const Text('Tab to run'), + title: const Text('Tap to run'), ), if (state.isAutomation) ...state.ifItems.map((item) { diff --git a/lib/pages/routines/models/ac/ac_function.dart b/lib/pages/routines/models/ac/ac_function.dart index 0b534e88..edc377dd 100644 --- a/lib/pages/routines/models/ac/ac_function.dart +++ b/lib/pages/routines/models/ac/ac_function.dart @@ -14,6 +14,10 @@ abstract class ACFunction extends DeviceFunction { required super.operationName, required super.icon, required this.type, + super.step, + super.unit, + super.max, + super.min, }); List getOperationalValues(); @@ -75,26 +79,24 @@ class ModeFunction extends ACFunction { } class TempSetFunction extends ACFunction { - final int min; - final int max; - final int step; - - TempSetFunction( - {required super.deviceId, required super.deviceName, required type}) - : min = 160, - max = 300, - step = 1, - super( + TempSetFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( code: 'temp_set', operationName: 'Set Temperature', icon: Assets.assetsTempreture, - type: type, + min: 200, + max: 300, + step: 1, + unit: "°C", ); @override List getOperationalValues() { List values = []; - for (int temp = min; temp <= max; temp += step) { + for (int temp = min!.toInt(); temp <= max!; temp += step!.toInt()) { values.add(ACOperationalValue( icon: Assets.assetsTempreture, description: "${temp / 10}°C", @@ -104,7 +106,6 @@ class TempSetFunction extends ACFunction { return values; } } - class LevelFunction extends ACFunction { LevelFunction( {required super.deviceId, required super.deviceName, required type}) @@ -166,9 +167,10 @@ class ChildLockFunction extends ACFunction { } class CurrentTempFunction extends ACFunction { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; + final String unit = "°C"; CurrentTempFunction( {required super.deviceId, required super.deviceName, required type}) @@ -185,7 +187,7 @@ class CurrentTempFunction extends ACFunction { @override List getOperationalValues() { List values = []; - for (int temp = min; temp <= max; temp += step) { + for (int temp = min.toInt(); temp <= max; temp += step.toInt()) { values.add(ACOperationalValue( icon: Assets.currentTemp, description: "${temp / 10}°C", diff --git a/lib/pages/routines/models/ceiling_presence_sensor_functions.dart b/lib/pages/routines/models/ceiling_presence_sensor_functions.dart index 6dbe5cf6..122d8ea1 100644 --- a/lib/pages/routines/models/ceiling_presence_sensor_functions.dart +++ b/lib/pages/routines/models/ceiling_presence_sensor_functions.dart @@ -6,10 +6,12 @@ class CpsOperationalValue { final String description; final dynamic value; + CpsOperationalValue({ required this.icon, required this.description, required this.value, + }); } @@ -94,9 +96,9 @@ final class CpsSensitivityFunction extends CpsFunctions { icon: Assets.sensitivity, ); - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; static const _images = [ Assets.sensitivityFeature1, @@ -115,10 +117,10 @@ final class CpsSensitivityFunction extends CpsFunctions { @override List getOperationalValues() { final values = []; - for (var value = min; value <= max; value += step) { + for (var value = min; value <= max; value += step.toInt()) { values.add( CpsOperationalValue( - icon: _images[value], + icon: _images[value.toInt()], description: '$value', value: value, ), @@ -142,9 +144,9 @@ final class CpsMovingSpeedFunction extends CpsFunctions { icon: Assets.speedoMeter, ); - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; @override List getOperationalValues() { @@ -173,9 +175,9 @@ final class CpsSpatialStaticValueFunction extends CpsFunctions { icon: Assets.spatialStaticValue, ); - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; @override List getOperationalValues() { @@ -204,9 +206,9 @@ final class CpsSpatialMotionValueFunction extends CpsFunctions { icon: Assets.spatialMotionValue, ); - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; @override List getOperationalValues() { @@ -375,9 +377,9 @@ final class CpsPresenceJudgementThrsholdFunction extends CpsFunctions { icon: Assets.presenceJudgementThrshold, ); - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; @override List getOperationalValues() { @@ -406,9 +408,9 @@ final class CpsMotionAmplitudeTriggerThresholdFunction extends CpsFunctions { icon: Assets.presenceJudgementThrshold, ); - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; @override List getOperationalValues() { diff --git a/lib/pages/routines/models/device_functions.dart b/lib/pages/routines/models/device_functions.dart index 59b63a4f..40b26304 100644 --- a/lib/pages/routines/models/device_functions.dart +++ b/lib/pages/routines/models/device_functions.dart @@ -4,6 +4,10 @@ abstract class DeviceFunction { final String code; final String operationName; final String icon; + final double? step; + final String? unit; + final double? max; + final double? min; DeviceFunction({ required this.deviceId, @@ -11,6 +15,10 @@ abstract class DeviceFunction { required this.code, required this.operationName, required this.icon, + this.step, + this.unit, + this.max, + this.min, }); } @@ -22,6 +30,10 @@ class DeviceFunctionData { final dynamic value; final String? condition; final String? valueDescription; + final double? step; + final String? unit; + final double? max; + final double? min; DeviceFunctionData({ required this.entityId, @@ -31,6 +43,10 @@ class DeviceFunctionData { required this.value, this.condition, this.valueDescription, + this.step, + this.unit, + this.max, + this.min, }); Map toJson() { @@ -42,6 +58,10 @@ class DeviceFunctionData { 'value': value, if (condition != null) 'condition': condition, if (valueDescription != null) 'valueDescription': valueDescription, + if (step != null) 'step': step, + if (unit != null) 'unit': unit, + if (max != null) 'max': max, + if (min != null) 'min': min, }; } @@ -54,6 +74,10 @@ class DeviceFunctionData { value: json['value'], condition: json['condition'], valueDescription: json['valueDescription'], + step: json['step']?.toDouble(), + unit: json['unit'], + max: json['max']?.toDouble(), + min: json['min']?.toDouble(), ); } @@ -68,7 +92,11 @@ class DeviceFunctionData { other.operationName == operationName && other.value == value && other.condition == condition && - other.valueDescription == valueDescription; + other.valueDescription == valueDescription && + other.step == step && + other.unit == unit && + other.max == max && + other.min == min; } @override @@ -79,6 +107,34 @@ class DeviceFunctionData { operationName.hashCode ^ value.hashCode ^ condition.hashCode ^ - valueDescription.hashCode; + valueDescription.hashCode ^ + step.hashCode ^ + unit.hashCode ^ + max.hashCode ^ + min.hashCode; + } + + DeviceFunctionData copyWith({ + String? entityId, + String? functionCode, + String? operationName, + String? condition, + dynamic value, + double? step, + String? unit, + double? max, + double? min, + }) { + return DeviceFunctionData( + entityId: entityId ?? this.entityId, + functionCode: functionCode ?? this.functionCode, + operationName: operationName ?? this.operationName, + condition: condition ?? this.condition, + value: value ?? this.value, + step: step ?? this.step, + unit: unit ?? this.unit, + max: max ?? this.max, + min: min ?? this.min, + ); } } diff --git a/lib/pages/routines/models/flush/flush_functions.dart b/lib/pages/routines/models/flush/flush_functions.dart index 5013c0b8..a8f6ccd4 100644 --- a/lib/pages/routines/models/flush/flush_functions.dart +++ b/lib/pages/routines/models/flush/flush_functions.dart @@ -20,12 +20,11 @@ abstract class FlushFunctions } class FlushPresenceDelayFunction extends FlushFunctions { - final int min; FlushPresenceDelayFunction({ required super.deviceId, required super.deviceName, required super.type, - }) : min = 0, + }) : super( code: FlushMountedPresenceSensorModel.codePresenceState, operationName: 'Presence State', @@ -50,9 +49,9 @@ class FlushPresenceDelayFunction extends FlushFunctions { } class FlushSensiReduceFunction extends FlushFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; FlushSensiReduceFunction({ required super.deviceId, @@ -80,8 +79,8 @@ class FlushSensiReduceFunction extends FlushFunctions { } class FlushNoneDelayFunction extends FlushFunctions { - final int min; - final int max; + final double min; + final double max; final String unit; FlushNoneDelayFunction({ @@ -110,9 +109,9 @@ class FlushNoneDelayFunction extends FlushFunctions { } class FlushIlluminanceFunction extends FlushFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; FlushIlluminanceFunction({ required super.deviceId, @@ -130,7 +129,7 @@ class FlushIlluminanceFunction extends FlushFunctions { @override List getOperationalValues() { List values = []; - for (int lux = min; lux <= max; lux += step) { + for (int lux = min.toInt(); lux <= max; lux += step.toInt()) { values.add(FlushOperationalValue( icon: Assets.IlluminanceIcon, description: "$lux Lux", @@ -142,9 +141,9 @@ class FlushIlluminanceFunction extends FlushFunctions { } class FlushOccurDistReduceFunction extends FlushFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; FlushOccurDistReduceFunction({ required super.deviceId, @@ -173,9 +172,9 @@ class FlushOccurDistReduceFunction extends FlushFunctions { // ==== then functions ==== class FlushSensitivityFunction extends FlushFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; FlushSensitivityFunction({ required super.deviceId, @@ -203,9 +202,9 @@ class FlushSensitivityFunction extends FlushFunctions { } class FlushNearDetectionFunction extends FlushFunctions { - final int min; + final double min; final double max; - final int step; + final double step; final String unit; FlushNearDetectionFunction({ @@ -225,7 +224,7 @@ class FlushNearDetectionFunction extends FlushFunctions { @override List getOperationalValues() { final values = []; - for (var value = min; value <= max; value += step) { + for (var value = min.toDouble(); value <= max; value += step) { values.add(FlushOperationalValue( icon: Assets.nobodyTime, description: '$value $unit', @@ -237,9 +236,9 @@ class FlushNearDetectionFunction extends FlushFunctions { } class FlushMaxDetectDistFunction extends FlushFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; final String unit; FlushMaxDetectDistFunction({ @@ -259,7 +258,7 @@ class FlushMaxDetectDistFunction extends FlushFunctions { @override List getOperationalValues() { final values = []; - for (var value = min; value <= max; value += step) { + for (var value = min; value <= max; value += step.toInt()) { values.add(FlushOperationalValue( icon: Assets.nobodyTime, description: '$value $unit', @@ -271,9 +270,9 @@ class FlushMaxDetectDistFunction extends FlushFunctions { } class FlushTargetConfirmTimeFunction extends FlushFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; final String unit; FlushTargetConfirmTimeFunction({ @@ -293,7 +292,7 @@ class FlushTargetConfirmTimeFunction extends FlushFunctions { @override List getOperationalValues() { final values = []; - for (var value = min; value <= max; value += step) { + for (var value = min.toDouble(); value <= max; value += step) { values.add(FlushOperationalValue( icon: Assets.nobodyTime, description: '$value $unit', @@ -305,9 +304,9 @@ class FlushTargetConfirmTimeFunction extends FlushFunctions { } class FlushDisappeDelayFunction extends FlushFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; final String unit; FlushDisappeDelayFunction({ @@ -327,7 +326,7 @@ class FlushDisappeDelayFunction extends FlushFunctions { @override List getOperationalValues() { final values = []; - for (var value = min; value <= max; value += step) { + for (var value = min.toDouble(); value <= max; value += step) { values.add(FlushOperationalValue( icon: Assets.nobodyTime, description: '$value $unit', @@ -339,9 +338,9 @@ class FlushDisappeDelayFunction extends FlushFunctions { } class FlushIndentLevelFunction extends FlushFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; final String unit; FlushIndentLevelFunction({ @@ -361,7 +360,7 @@ class FlushIndentLevelFunction extends FlushFunctions { @override List getOperationalValues() { final values = []; - for (var value = min; value <= max; value += step) { + for (var value = min.toDouble(); value <= max; value += step) { values.add(FlushOperationalValue( icon: Assets.nobodyTime, description: '$value $unit', @@ -373,9 +372,9 @@ class FlushIndentLevelFunction extends FlushFunctions { } class FlushTriggerLevelFunction extends FlushFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; final String unit; FlushTriggerLevelFunction({ @@ -395,7 +394,7 @@ class FlushTriggerLevelFunction extends FlushFunctions { @override List getOperationalValues() { final values = []; - for (var value = min; value <= max; value += step) { + for (var value = min.toDouble(); value <= max; value += step) { values.add(FlushOperationalValue( icon: Assets.nobodyTime, description: '$value $unit', diff --git a/lib/pages/routines/models/pc/energy_clamp_functions.dart b/lib/pages/routines/models/pc/energy_clamp_functions.dart new file mode 100644 index 00000000..4bf3ddd8 --- /dev/null +++ b/lib/pages/routines/models/pc/energy_clamp_functions.dart @@ -0,0 +1,416 @@ +import 'package:syncrow_web/pages/device_managment/power_clamp/models/power_clamp_batch_model.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/models/pc/enrgy_clamp_operational_value.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +abstract class EnergyClampFunctions extends DeviceFunction { + final String type; + + EnergyClampFunctions({ + required super.deviceId, + required super.deviceName, + required super.code, + required super.operationName, + required super.icon, + required this.type, + super.step, + super.unit, + super.max, + super.min, + }); + + List getOperationalValues(); +} + +// General & shared +class TotalEnergyConsumedStatusFunction extends EnergyClampFunctions { + TotalEnergyConsumedStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'EnergyConsumed', + operationName: 'Total Energy Consumed', + icon: Assets.energyConsumedIcon, + min: 0.00, + max: 20000000.00, + step: 1, + unit: "kWh", + ); + + @override + List getOperationalValues() => []; +} + +class TotalActivePowerConsumedStatusFunction extends EnergyClampFunctions { + TotalActivePowerConsumedStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'ActivePower', + operationName: 'Total Active Power', + icon: Assets.powerActiveIcon, + min: -19800000, + max: 19800000, + step: 0.1, + unit: "kW", + ); + + @override + List getOperationalValues() => []; +} + +class VoltagePhaseSequenceDetectionFunction extends EnergyClampFunctions { + VoltagePhaseSequenceDetectionFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'voltage_phase_seq', + operationName: 'Voltage phase sequence detection', + icon: Assets.voltageIcon, + ); + + @override + List getOperationalValues() => [ + EnergyClampOperationalValue( + icon: Assets.voltageIcon, description: '0', value: '0'), + EnergyClampOperationalValue( + icon: Assets.voltageIcon, description: '1', value: '1'), + EnergyClampOperationalValue( + icon: Assets.voltageIcon, description: '2', value: '2'), + EnergyClampOperationalValue( + icon: Assets.voltageIcon, description: '3', value: '3'), + EnergyClampOperationalValue( + icon: Assets.voltageIcon, description: '4', value: '4'), + EnergyClampOperationalValue( + icon: Assets.voltageIcon, description: '5', value: '5'), + ]; +} + +class TotalCurrentStatusFunction extends EnergyClampFunctions { + TotalCurrentStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'Current', + operationName: 'Total Current', + icon: Assets.voltMeterIcon, + min: 0.000, + max: 9000.000, + step: 1, + unit: "A", + ); + + @override + List getOperationalValues() => []; +} + +class FrequencyStatusFunction extends EnergyClampFunctions { + FrequencyStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'Frequency', + operationName: 'Frequency', + icon: Assets.frequencyIcon, + min: 0, + max: 80, + step: 1, + unit: "Hz", + ); + + @override + List getOperationalValues() => []; +} + +// Phase A +class EnergyConsumedAStatusFunction extends EnergyClampFunctions { + EnergyConsumedAStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'EnergyConsumedA', + operationName: 'Energy Consumed A', + icon: Assets.energyConsumedIcon, + min: 0.00, + max: 20000000.00, + step: 1, + unit: "kWh", + ); + + @override + List getOperationalValues() => []; +} + +class ActivePowerAStatusFunction extends EnergyClampFunctions { + ActivePowerAStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'ActivePowerA', + operationName: 'Active Power A', + icon: Assets.powerActiveIcon, + min: 200, + max: 300, + step: 1, + unit: "kW", + ); + + @override + List getOperationalValues() => []; +} + +class VoltageAStatusFunction extends EnergyClampFunctions { + VoltageAStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'VoltageA', + operationName: 'Voltage A', + icon: Assets.voltageIcon, + min: 0.0, + max: 500, + step: 1, + unit: "V", + ); + + @override + List getOperationalValues() => []; +} + +class PowerFactorAStatusFunction extends EnergyClampFunctions { + PowerFactorAStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'PowerFactorA', + operationName: 'Power Factor A', + icon: Assets.speedoMeter, + min: 0.00, + max: 1.00, + step: 0.1, + unit: "", + ); + + @override + List getOperationalValues() => []; +} + +class CurrentAStatusFunction extends EnergyClampFunctions { + CurrentAStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'CurrentA', + operationName: 'Current A', + icon: Assets.voltMeterIcon, + min: 0.000, + max: 3000.000, + step: 1, + unit: "A", + ); + + @override + List getOperationalValues() => []; +} + +// Phase B +class EnergyConsumedBStatusFunction extends EnergyClampFunctions { + EnergyConsumedBStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'EnergyConsumedB', + operationName: 'Energy Consumed B', + icon: Assets.energyConsumedIcon, + min: 0.00, + max: 20000000.00, + step: 1, + unit: "kWh", + ); + + @override + List getOperationalValues() => []; +} + +class ActivePowerBStatusFunction extends EnergyClampFunctions { + ActivePowerBStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'ActivePowerB', + operationName: 'Active Power B', + icon: Assets.powerActiveIcon, + min: -6600000, + max: 6600000, + step: 1, + unit: "kW", + ); + + @override + List getOperationalValues() => []; +} + +class VoltageBStatusFunction extends EnergyClampFunctions { + VoltageBStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'VoltageB', + operationName: 'Voltage B', + icon: Assets.voltageIcon, + min: 0.0, + max: 500, + step: 1, + unit: "V", + ); + + @override + List getOperationalValues() => []; +} + +class CurrentBStatusFunction extends EnergyClampFunctions { + CurrentBStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'CurrentB', + operationName: 'Current B', + icon: Assets.voltMeterIcon, + min: 0.000, + max: 3000.000, + step: 1, + unit: "A", + ); + + @override + List getOperationalValues() => []; +} + +class PowerFactorBStatusFunction extends EnergyClampFunctions { + PowerFactorBStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'PowerFactorB', + operationName: 'Power Factor B', + icon: Assets.speedoMeter, + min: 0.0, + max: 1.0, + step: 0.1, + unit: "", + ); + + @override + List getOperationalValues() => []; +} + +// Phase C +class EnergyConsumedCStatusFunction extends EnergyClampFunctions { + EnergyConsumedCStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'EnergyConsumedC', + operationName: 'Energy Consumed C', + icon: Assets.energyConsumedIcon, + min: 0.00, + max: 20000000.00, + step: 1, + unit: "kWh", + ); + + @override + List getOperationalValues() => []; +} + +class ActivePowerCStatusFunction extends EnergyClampFunctions { + ActivePowerCStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'ActivePowerC', + operationName: 'Active Power C', + icon: Assets.powerActiveIcon, + min: -6600000, + max: 6600000, + step: 1, + unit: "kW", + ); + + @override + List getOperationalValues() => []; +} + +class VoltageCStatusFunction extends EnergyClampFunctions { + VoltageCStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'VoltageC', + operationName: 'Voltage C', + icon: Assets.voltageIcon, + min: 0.00, + max: 500, + step: 0.1, + unit: "V", + ); + + @override + List getOperationalValues() => []; +} + +class CurrentCStatusFunction extends EnergyClampFunctions { + CurrentCStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'CurrentC', + operationName: 'Current C', + icon: Assets.voltMeterIcon, + min: 0.000, + max: 3000.000, + step: 0.1, + unit: "A", + ); + + @override + List getOperationalValues() => []; +} + +class PowerFactorCStatusFunction extends EnergyClampFunctions { + PowerFactorCStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'PowerFactorC', + operationName: 'Power Factor C', + icon: Assets.speedoMeter, + min: 0.00, + max: 1.00, + step: 0.1, + unit: "", + ); + + @override + List getOperationalValues() => []; +} diff --git a/lib/pages/routines/models/pc/enrgy_clamp_operational_value.dart b/lib/pages/routines/models/pc/enrgy_clamp_operational_value.dart new file mode 100644 index 00000000..5d89acf6 --- /dev/null +++ b/lib/pages/routines/models/pc/enrgy_clamp_operational_value.dart @@ -0,0 +1,11 @@ +class EnergyClampOperationalValue { + final String icon; + final String description; + final dynamic value; + + EnergyClampOperationalValue({ + required this.icon, + required this.description, + required this.value, + }); +} diff --git a/lib/pages/routines/models/water_heater/water_heater_functions.dart b/lib/pages/routines/models/water_heater/water_heater_functions.dart new file mode 100644 index 00000000..7ebea019 --- /dev/null +++ b/lib/pages/routines/models/water_heater/water_heater_functions.dart @@ -0,0 +1,130 @@ +import 'package:syncrow_web/pages/device_managment/water_heater/models/water_heater_status_model.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/models/water_heater/water_heater_operational_value.dart'; +import 'package:syncrow_web/utils/constants/assets.dart'; + +abstract class WaterHeaterFunctions + extends DeviceFunction { + final String type; + + WaterHeaterFunctions({ + required super.deviceId, + required super.deviceName, + required super.code, + required super.operationName, + required super.icon, + required this.type, + }); + + List getOperationalValues(); +} + +class WHRestartStatusFunction extends WaterHeaterFunctions { + WHRestartStatusFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'relay_status', + operationName: 'Restart Status', + icon: Assets.refreshStatusIcon, + ); + + + @override + List getOperationalValues() { + return [ + WaterHeaterOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: 'Power OFF', + value: "off", + ), + WaterHeaterOperationalValue( + icon: Assets.assetsAcPower, + description: 'Power ON', + value: 'on', + ), + WaterHeaterOperationalValue( + icon: Assets.refreshStatusIcon, + description: "Restart Memory", + value: 'memory', + ), + ]; + } +} + +class WHSwitchFunction extends WaterHeaterFunctions { + WHSwitchFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'switch_1', + operationName: 'Switch', + icon: Assets.assetsAcPower, + ); + + @override + List getOperationalValues() { + return [ + WaterHeaterOperationalValue( + icon: Assets.assetsAcPower, + description: 'ON', + value: true, + ), + WaterHeaterOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: 'OFF', + value: false, + ), + ]; + } +} + +class TimerConfirmTimeFunction extends WaterHeaterFunctions { + TimerConfirmTimeFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : super( + code: 'countdown_1', + operationName: 'Timer', + icon: Assets.targetConfirmTimeIcon, + ); + + @override + List getOperationalValues() { + final values = []; + + return values; + } +} + +class BacklightFunction extends WaterHeaterFunctions { + BacklightFunction({ + required super.deviceId, + required super.deviceName, + required super.type, + }) : + super( + code: 'switch_backlight', + operationName: 'Backlight', + icon: Assets.indicator, + ); + + @override + List getOperationalValues() { + return [ + WaterHeaterOperationalValue( + icon: Assets.assetsAcPower, + description: 'ON', + value: true, + ), + WaterHeaterOperationalValue( + icon: Assets.assetsAcPowerOFF, + description: 'OFF', + value: false, + ), + ]; + } +} diff --git a/lib/pages/routines/models/water_heater/water_heater_operational_value.dart b/lib/pages/routines/models/water_heater/water_heater_operational_value.dart new file mode 100644 index 00000000..dd1e5157 --- /dev/null +++ b/lib/pages/routines/models/water_heater/water_heater_operational_value.dart @@ -0,0 +1,11 @@ +class WaterHeaterOperationalValue { + final String icon; + final String description; + final dynamic value; + + WaterHeaterOperationalValue({ + required this.icon, + required this.description, + required this.value, + }); +} diff --git a/lib/pages/routines/models/wps/wps_functions.dart b/lib/pages/routines/models/wps/wps_functions.dart index 8907927c..101c5cf0 100644 --- a/lib/pages/routines/models/wps/wps_functions.dart +++ b/lib/pages/routines/models/wps/wps_functions.dart @@ -4,7 +4,7 @@ import 'package:syncrow_web/pages/routines/models/wps/wps_operational_value.dart import 'package:syncrow_web/utils/constants/assets.dart'; abstract class WpsFunctions extends DeviceFunction { - final String type; + final String type; WpsFunctions({ required super.deviceId, @@ -13,6 +13,10 @@ abstract class WpsFunctions extends DeviceFunction { required super.operationName, required super.icon, required this.type, + super.step, + super.unit, + super.max, + super.min, }); List getOperationalValues(); @@ -20,9 +24,13 @@ abstract class WpsFunctions extends DeviceFunction { // For far_detection (75-600cm in 75cm steps) class FarDetectionFunction extends WpsFunctions { - final int min; - final int max; - final int step; + + final double min; + @override + final double max; + @override + final double step; + @override final String unit; FarDetectionFunction( @@ -41,7 +49,7 @@ class FarDetectionFunction extends WpsFunctions { @override List getOperationalValues() { final values = []; - for (var value = min; value <= max; value += step) { + for (var value = min; value <= max; value += step.toInt()) { values.add(WpsOperationalValue( icon: Assets.currentDistanceIcon, description: '$value $unit', @@ -54,9 +62,9 @@ class FarDetectionFunction extends WpsFunctions { // For presence_time (0-65535 minutes) class PresenceTimeFunction extends WpsFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; final String unit; PresenceTimeFunction( @@ -86,9 +94,9 @@ class PresenceTimeFunction extends WpsFunctions { // For motion_sensitivity_value (1-5 levels) class MotionSensitivityFunction extends WpsFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; MotionSensitivityFunction( {required super.deviceId, required super.deviceName, required type}) @@ -116,9 +124,9 @@ class MotionSensitivityFunction extends WpsFunctions { } class MotionLessSensitivityFunction extends WpsFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; MotionLessSensitivityFunction( {required super.deviceId, required super.deviceName, required type}) @@ -171,8 +179,8 @@ class IndicatorFunction extends WpsFunctions { } class NoOneTimeFunction extends WpsFunctions { - final int min; - final int max; + final double min; + final double max; final String unit; NoOneTimeFunction( @@ -225,9 +233,9 @@ class PresenceStateFunction extends WpsFunctions { } class CurrentDistanceFunction extends WpsFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; CurrentDistanceFunction( {required super.deviceId, required super.deviceName, required type}) @@ -244,11 +252,10 @@ class CurrentDistanceFunction extends WpsFunctions { @override List getOperationalValues() { List values = []; - for (int cm = min; cm <= max; cm += step) { + for (int cm = min.toInt(); cm <= max; cm += step.toInt()) { values.add(WpsOperationalValue( icon: Assets.assetsTempreture, description: "${cm}CM", - value: cm, )); } @@ -257,9 +264,9 @@ class CurrentDistanceFunction extends WpsFunctions { } class IlluminanceValueFunction extends WpsFunctions { - final int min; - final int max; - final int step; + final double min; + final double max; + final double step; IlluminanceValueFunction({ required super.deviceId, @@ -277,7 +284,7 @@ class IlluminanceValueFunction extends WpsFunctions { @override List getOperationalValues() { List values = []; - for (int lux = min; lux <= max; lux += step) { + for (int lux = min.toInt(); lux <= max; lux += step.toInt()) { values.add(WpsOperationalValue( icon: Assets.IlluminanceIcon, description: "$lux Lux", diff --git a/lib/pages/routines/widgets/condition_toggle.dart b/lib/pages/routines/widgets/condition_toggle.dart index 99ea2f04..541ad431 100644 --- a/lib/pages/routines/widgets/condition_toggle.dart +++ b/lib/pages/routines/widgets/condition_toggle.dart @@ -12,22 +12,53 @@ class ConditionToggle extends StatelessWidget { }); static const _conditions = ["<", "==", ">"]; + static const _icons = [ + Icons.chevron_left, + Icons.drag_handle, + Icons.chevron_right + ]; @override Widget build(BuildContext context) { - return ToggleButtons( - onPressed: (index) => onChanged(_conditions[index]), - borderRadius: const BorderRadius.all(Radius.circular(8)), - selectedBorderColor: ColorsManager.primaryColorWithOpacity, - selectedColor: Colors.white, - fillColor: ColorsManager.primaryColorWithOpacity, - color: ColorsManager.primaryColorWithOpacity, - constraints: const BoxConstraints( - minHeight: 40.0, - minWidth: 40.0, + final selectedIndex = _conditions.indexOf(currentCondition ?? "=="); + + return Container( + height: 30, + width: MediaQuery.of(context).size.width * 0.1, + decoration: BoxDecoration( + color: ColorsManager.softGray.withOpacity(0.5), + borderRadius: BorderRadius.circular(50), + ), + clipBehavior: Clip.antiAlias, + child: Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(_conditions.length, (index) { + final isSelected = index == selectedIndex; + return Expanded( + child: InkWell( + onTap: () => onChanged(_conditions[index]), + child: AnimatedContainer( + duration: const Duration(milliseconds: 180), + curve: Curves.ease, + decoration: BoxDecoration( + color: + isSelected ? ColorsManager.vividBlue : Colors.transparent, + ), + child: Center( + child: Icon( + _icons[index], + size: 20, + color: isSelected + ? ColorsManager.whiteColors + : ColorsManager.blackColor, + weight: isSelected ? 700 : 500, + ), + ), + ), + ), + ); + }), ), - isSelected: _conditions.map((c) => c == (currentCondition ?? "==")).toList(), - children: _conditions.map((c) => Text(c)).toList(), ); } } diff --git a/lib/pages/routines/widgets/conditions_routines_devices_view.dart b/lib/pages/routines/widgets/conditions_routines_devices_view.dart index 3def44de..4088cdf7 100644 --- a/lib/pages/routines/widgets/conditions_routines_devices_view.dart +++ b/lib/pages/routines/widgets/conditions_routines_devices_view.dart @@ -29,11 +29,11 @@ class ConditionsRoutinesDevicesView extends StatelessWidget { children: [ DraggableCard( imagePath: Assets.tabToRun, - title: 'Tab to run', + title: 'Tap to run', deviceData: { 'deviceId': 'tab_to_run', 'type': 'trigger', - 'name': 'Tab to run', + 'name': 'Tap to run', }, ), DraggableCard( diff --git a/lib/pages/routines/widgets/custom_routines_textbox.dart b/lib/pages/routines/widgets/custom_routines_textbox.dart new file mode 100644 index 00000000..f0767df4 --- /dev/null +++ b/lib/pages/routines/widgets/custom_routines_textbox.dart @@ -0,0 +1,330 @@ +import 'dart:math'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:syncrow_web/pages/routines/widgets/condition_toggle.dart'; +import 'package:syncrow_web/pages/routines/widgets/slider_value_selector.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class CustomRoutinesTextbox extends StatefulWidget { + final String? currentCondition; + final String dialogType; + final (double, double) sliderRange; + final dynamic displayedValue; + final dynamic initialValue; + final void Function(String condition) onConditionChanged; + final void Function(double value) onTextChanged; + final String unit; + final double dividendOfRange; + final double stepIncreaseAmount; + final bool withSpecialChar; + + const CustomRoutinesTextbox({ + required this.dialogType, + required this.sliderRange, + required this.displayedValue, + required this.initialValue, + required this.onConditionChanged, + required this.onTextChanged, + required this.currentCondition, + required this.unit, + required this.dividendOfRange, + required this.stepIncreaseAmount, + required this.withSpecialChar, + super.key, + }); + + @override + State createState() => _CustomRoutinesTextboxState(); +} + +class _CustomRoutinesTextboxState extends State { + late final TextEditingController _controller; + + bool hasError = false; + String? errorMessage; + + int getDecimalPlaces(double step) { + String stepStr = step.toString(); + if (stepStr.contains('.')) { + List parts = stepStr.split('.'); + String decimalPart = parts[1]; + decimalPart = decimalPart.replaceAll(RegExp(r'0+$'), ''); + return decimalPart.isEmpty ? 0 : decimalPart.length; + } else { + return 0; + } + } + + bool _isInitialized = false; + + @override + void initState() { + super.initState(); + _initializeController(); + } + + void _initializeController() { + final decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount); + final dynamic initialValue = widget.initialValue; + double parsedValue; + + if (initialValue is num) { + parsedValue = initialValue.toDouble(); + } else if (initialValue is String) { + parsedValue = double.tryParse(initialValue) ?? widget.sliderRange.$1; + } else { + parsedValue = widget.sliderRange.$1; + } + + _controller = TextEditingController( + text: parsedValue.toStringAsFixed(decimalPlaces), + ); + _isInitialized = true; + } + + @override + void didUpdateWidget(CustomRoutinesTextbox oldWidget) { + super.didUpdateWidget(oldWidget); + + if (widget.initialValue != oldWidget.initialValue && _isInitialized) { + final decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount); + final dynamic initialValue = widget.initialValue; + double newValue; + + if (initialValue is num) { + newValue = initialValue.toDouble(); + } else if (initialValue is String) { + newValue = double.tryParse(initialValue) ?? widget.sliderRange.$1; + } else { + newValue = widget.sliderRange.$1; + } + + final newValueText = newValue.toStringAsFixed(decimalPlaces); + if (_controller.text != newValueText) { + WidgetsBinding.instance.addPostFrameCallback((_) { + _controller.text = newValueText; + _controller.selection = + TextSelection.collapsed(offset: _controller.text.length); + }); + } + } + } + + + + void _validateInput(String value) { + final doubleValue = double.tryParse(value); + if (doubleValue == null) { + setState(() { + errorMessage = "Invalid number"; + hasError = true; + }); + return; + } + + final min = widget.sliderRange.$1; + final max = widget.sliderRange.$2; + + if (doubleValue < min) { + setState(() { + errorMessage = "Value must be at least $min"; + hasError = true; + }); + } else if (doubleValue > max) { + setState(() { + errorMessage = "Value must be at most $max"; + hasError = true; + }); + } else { + int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount); + int factor = pow(10, decimalPlaces).toInt(); + int scaledStep = (widget.stepIncreaseAmount * factor).round(); + int scaledValue = (doubleValue * factor).round(); + + if (scaledValue % scaledStep != 0) { + setState(() { + errorMessage = "must be a multiple of ${widget.stepIncreaseAmount}"; + hasError = true; + }); + } else { + setState(() { + errorMessage = null; + hasError = false; + }); + } + } + } + + + void _correctAndUpdateValue(String value) { + final doubleValue = double.tryParse(value) ?? 0.0; + int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount); + double rounded = (doubleValue / widget.stepIncreaseAmount).round() * + widget.stepIncreaseAmount; + rounded = rounded.clamp(widget.sliderRange.$1, widget.sliderRange.$2); + rounded = double.parse(rounded.toStringAsFixed(decimalPlaces)); + + setState(() { + hasError = false; + errorMessage = null; + }); + + _controller.text = rounded.toStringAsFixed(decimalPlaces); + _controller.selection = TextSelection.fromPosition( + TextPosition(offset: _controller.text.length), + ); + widget.onTextChanged(rounded); + } + + @override + Widget build(BuildContext context) { + int decimalPlaces = getDecimalPlaces(widget.stepIncreaseAmount); + + List formatters = []; + if (decimalPlaces == 0) { + formatters.add(FilteringTextInputFormatter.digitsOnly); + } else { + formatters.add(FilteringTextInputFormatter.allow( + RegExp(r'^\d*\.?\d{0,' + decimalPlaces.toString() + r'}$'), + )); + } + formatters.add(RangeInputFormatter( + min: widget.sliderRange.$1, + max: widget.sliderRange.$2, + )); + + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (widget.dialogType == 'IF') + ConditionToggle( + currentCondition: widget.currentCondition, + onChanged: widget.onConditionChanged, + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 35, vertical: 2), + child: Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.end, + children: [ + Text( + 'Step: ${widget.stepIncreaseAmount}', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.grayColor, + fontSize: 10, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + Center( + child: Container( + width: 170, + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + color: const Color(0xFFF8F8F8), + borderRadius: BorderRadius.circular(20), + border: hasError + ? Border.all(color: Colors.red, width: 1) + : Border.all( + color: ColorsManager.lightGrayBorderColor, width: 1), + boxShadow: [ + BoxShadow( + color: ColorsManager.blackColor.withOpacity(0.05), + blurRadius: 8, + offset: const Offset(0, 4), + ), + ], + ), + child: Row( + children: [ + Expanded( + child: TextFormField( + controller: _controller, + style: context.textTheme.bodyLarge?.copyWith( + fontSize: 20, + fontWeight: FontWeight.bold, + color: ColorsManager.blackColor, + ), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow( + widget.withSpecialChar + ? RegExp(r'^-?\d*\.?\d{0,' + + decimalPlaces.toString() + + r'}$') + : RegExp(r'\d+'), + ), + ], + decoration: const InputDecoration( + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + onChanged: _validateInput, + onFieldSubmitted: _correctAndUpdateValue, + onTapOutside: (_) => + _correctAndUpdateValue(_controller.text), + ), + ), + const SizedBox(width: 12), + Text( + widget.unit, + style: context.textTheme.bodyMedium?.copyWith( + fontSize: 20, + fontWeight: FontWeight.bold, + color: ColorsManager.vividBlue, + ), + ), + ], + ), + ), + ), + if (errorMessage != null) + Padding( + padding: const EdgeInsets.only(top: 2.0), + child: Text( + errorMessage!, + style: context.textTheme.bodySmall?.copyWith( + color: Colors.red, + fontSize: 10, + ), + ), + ), + const SizedBox(height: 16), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 32), + child: Wrap( + alignment: WrapAlignment.spaceBetween, + direction: Axis.horizontal, + children: [ + Text( + 'Min. ${widget.sliderRange.$1.toInt()}${widget.unit}', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.grayColor, + fontSize: 10, + fontWeight: FontWeight.w400, + ), + ), + const SizedBox( + width: 50, + ), + Text( + 'Max. ${widget.sliderRange.$2.toInt()}${widget.unit}', + style: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.grayColor, + fontSize: 10, + fontWeight: FontWeight.w400, + ), + ), + ], + ), + ), + const SizedBox(height: 16), + ], + ); + } +} diff --git a/lib/pages/routines/widgets/if_container.dart b/lib/pages/routines/widgets/if_container.dart index 884c8d10..da77c7c2 100644 --- a/lib/pages/routines/widgets/if_container.dart +++ b/lib/pages/routines/widgets/if_container.dart @@ -43,7 +43,7 @@ class IfContainer extends StatelessWidget { children: [ DraggableCard( imagePath: Assets.tabToRun, - title: 'Tab to run', + title: 'Tap to run', deviceData: {}, ), ], @@ -76,10 +76,11 @@ class IfContainer extends StatelessWidget { 'WPS', 'GW', 'CPS', - 'NCPS' + 'NCPS', + 'WH', + 'PC', ].contains(state.ifItems[index] ['productType'])) { - context.read().add( AddToIfContainer( state.ifItems[index], false)); @@ -136,8 +137,18 @@ class IfContainer extends StatelessWidget { context .read() .add(AddToIfContainer(mutableData, false)); - } else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS', 'NCPS'] - .contains(mutableData['productType'])) { + } else if (![ + 'AC', + '1G', + '2G', + '3G', + 'WPS', + 'GW', + 'CPS', + 'NCPS', + 'WH', + 'PC', + ].contains(mutableData['productType'])) { context .read() .add(AddToIfContainer(mutableData, false)); diff --git a/lib/pages/routines/widgets/main_routine_view/fetch_routine_scenes_automation.dart b/lib/pages/routines/widgets/main_routine_view/fetch_routine_scenes_automation.dart index a67caef9..f9c20c54 100644 --- a/lib/pages/routines/widgets/main_routine_view/fetch_routine_scenes_automation.dart +++ b/lib/pages/routines/widgets/main_routine_view/fetch_routine_scenes_automation.dart @@ -26,7 +26,7 @@ class FetchRoutineScenesAutomation extends StatelessWidget crossAxisAlignment: CrossAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ - _buildListTitle(context, "Scenes (Tab to Run)"), + _buildListTitle(context, "Scenes (Tap to Run)"), const SizedBox(height: 10), Visibility( visible: state.scenes.isNotEmpty, diff --git a/lib/pages/routines/widgets/routine_devices.dart b/lib/pages/routines/widgets/routine_devices.dart index 9192b422..f0b77467 100644 --- a/lib/pages/routines/widgets/routine_devices.dart +++ b/lib/pages/routines/widgets/routine_devices.dart @@ -25,7 +25,9 @@ class _RoutineDevicesState extends State { 'WPS', 'GW', 'CPS', - 'NCPS' + 'NCPS', + 'WH', + 'PC', }; @override diff --git a/lib/pages/routines/widgets/routine_dialogs/ac_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/ac_dialog.dart index 1de9b0d4..6d0ddbfb 100644 --- a/lib/pages/routines/widgets/routine_dialogs/ac_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/ac_dialog.dart @@ -7,6 +7,7 @@ import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; import 'package:syncrow_web/pages/routines/models/ac/ac_function.dart'; import 'package:syncrow_web/pages/routines/models/ac/ac_operational_value.dart'; import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/custom_routines_textbox.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart'; @@ -64,7 +65,9 @@ class ACHelper { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const DialogHeader('AC Functions'), + DialogHeader(dialogType == 'THEN' + ? 'AC Functions' + : 'AC Conditions'), Expanded( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -73,29 +76,24 @@ class ACHelper { SizedBox( width: selectedFunction != null ? 320 : 360, child: _buildFunctionsList( - context: context, - acFunctions: acFunctions, - device: device, - onFunctionSelected: (functionCode, operationName) { - RoutineTapFunctionHelper.onTapFunction( - context, - functionCode: functionCode, - functionOperationName: operationName, - functionValueDescription: - selectedFunctionData.valueDescription, - deviceUuid: device?.uuid, - codesToAddIntoFunctionsWithDefaultValue: [ - 'temp_set', - 'temp_current', - ], - defaultValue: functionCode == 'temp_set' - ? 200 - : functionCode == 'temp_current' - ? -100 - : 0, - ); - }, - ), + context: context, + acFunctions: acFunctions, + onFunctionSelected: + (functionCode, operationName) { + RoutineTapFunctionHelper.onTapFunction( + context, + functionCode: functionCode, + functionOperationName: operationName, + functionValueDescription: + selectedFunctionData + .valueDescription, + deviceUuid: device?.uuid, + codesToAddIntoFunctionsWithDefaultValue: [ + 'temp_set', + 'temp_current', + ], + defaultValue: 0); + }), ), // Value selector if (selectedFunction != null) @@ -153,7 +151,6 @@ class ACHelper { required BuildContext context, required List acFunctions, required Function(String, String) onFunctionSelected, - required AllDevicesModel? device, }) { return ListView.separated( shrinkWrap: false, @@ -196,7 +193,6 @@ class ACHelper { ); } - /// Build value selector for AC functions dialog static Widget _buildValueSelector({ required BuildContext context, required String selectedFunction, @@ -206,24 +202,63 @@ class ACHelper { required String operationName, bool? removeComparators, }) { - final initialVal = selectedFunction == 'temp_set' ? 200 : -100; + final selectedFn = + acFunctions.firstWhere((f) => f.code == selectedFunction); + if (selectedFunction == 'temp_set' || selectedFunction == 'temp_current') { - final initialValue = selectedFunctionData?.value ?? initialVal; - return _buildTemperatureSelector( - context: context, - initialValue: initialValue, - selectCode: selectedFunction, + final displayValue = + (selectedFunctionData?.value ?? selectedFn.min!) / 10; + final minValue = selectedFn.min! / 10; + final maxValue = selectedFn.max! / 10; + + return CustomRoutinesTextbox( + withSpecialChar: true, + dividendOfRange: maxValue, currentCondition: selectedFunctionData?.condition, - device: device, - operationName: operationName, - selectedFunctionData: selectedFunctionData, - removeComparators: removeComparators, + dialogType: selectedFn.type, + sliderRange: (minValue, maxValue), + displayedValue: displayValue.toString(), + initialValue: displayValue, + unit: selectedFn.unit!, + onConditionChanged: (condition) => context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectedFunction, + operationName: selectedFn.operationName, + condition: condition, + value: (displayValue * 10).round(), + step: selectedFn.step, + unit: selectedFn.unit, + max: selectedFn.max, + min: selectedFn.min, + ), + ), + ), + onTextChanged: (value) { + final numericValue = double.tryParse(value.toString()) ?? minValue; + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectedFunction, + operationName: selectedFn.operationName, + value: (numericValue * 10).round(), + condition: selectedFunctionData?.condition, + step: selectedFn.step, + unit: selectedFn.unit, + max: selectedFn.max, + min: selectedFn.min, + ), + ), + ); + }, + stepIncreaseAmount: selectedFn.step! / 10, ); } - final selectedFn = acFunctions.firstWhere((f) => f.code == selectedFunction); + // Rest of your existing code for other value selectors final values = selectedFn.getOperationalValues(); - return _buildOperationalValuesList( context: context, values: values, @@ -235,150 +270,151 @@ class ACHelper { ); } - /// Build temperature selector for AC functions dialog - static Widget _buildTemperatureSelector({ - required BuildContext context, - required dynamic initialValue, - required String? currentCondition, - required String selectCode, - AllDevicesModel? device, - required String operationName, - DeviceFunctionData? selectedFunctionData, - bool? removeComparators, - }) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (removeComparators != true) - _buildConditionToggle( - context, - currentCondition, - selectCode, - device, - operationName, - selectedFunctionData, - ), - const SizedBox(height: 20), - _buildTemperatureDisplay( - context, - initialValue, - device, - operationName, - selectedFunctionData, - selectCode, - ), - const SizedBox(height: 20), - _buildTemperatureSlider( - context, - initialValue, - device, - operationName, - selectedFunctionData, - selectCode, - ), - ], - ); - } + // /// Build temperature selector for AC functions dialog + // static Widget _buildTemperatureSelector({ + // required BuildContext context, + // required dynamic initialValue, + // required String? currentCondition, + // required String selectCode, + // AllDevicesModel? device, + // required String operationName, + // DeviceFunctionData? selectedFunctionData, + // bool? removeComparators, + // }) { + // return Column( + // mainAxisAlignment: MainAxisAlignment.center, + // children: [ + // if (removeComparators != true) + // _buildConditionToggle( + // context, + // currentCondition, + // selectCode, + // device, + // operationName, + // selectedFunctionData, + // ), + // const SizedBox(height: 20), + // _buildTemperatureDisplay( + // context, + // initialValue, + // device, + // operationName, + // selectedFunctionData, + // selectCode, + // ), + // const SizedBox(height: 20), + // _buildTemperatureSlider( + // context, + // initialValue, + // device, + // operationName, + // selectedFunctionData, + // selectCode, + // ), + // ], + // ); + // } /// Build condition toggle for AC functions dialog - static Widget _buildConditionToggle( - BuildContext context, - String? currentCondition, - String selectCode, - AllDevicesModel? device, - String operationName, - DeviceFunctionData? selectedFunctionData, + // static Widget _buildConditionToggle( + // BuildContext context, + // String? currentCondition, + // String selectCode, + // AllDevicesModel? device, + // String operationName, + // DeviceFunctionData? selectedFunctionData, - // Function(String) onConditionChanged, - ) { - final conditions = ["<", "==", ">"]; + // // Function(String) onConditionChanged, + // ) { + // final conditions = ["<", "==", ">"]; - return ToggleButtons( - onPressed: (int index) { - context.read().add( - AddFunction( - functionData: DeviceFunctionData( - entityId: device?.uuid ?? '', - functionCode: selectCode, - operationName: operationName, - condition: conditions[index], - value: selectedFunctionData?.value ?? selectCode == 'temp_set' - ? 200 - : -100, - valueDescription: selectedFunctionData?.valueDescription, - ), - ), - ); - }, - borderRadius: const BorderRadius.all(Radius.circular(8)), - selectedBorderColor: ColorsManager.primaryColorWithOpacity, - selectedColor: Colors.white, - fillColor: ColorsManager.primaryColorWithOpacity, - color: ColorsManager.primaryColorWithOpacity, - constraints: const BoxConstraints( - minHeight: 40.0, - minWidth: 40.0, - ), - isSelected: conditions.map((c) => c == (currentCondition ?? "==")).toList(), - children: conditions.map((c) => Text(c)).toList(), - ); - } + // return ToggleButtons( + // onPressed: (int index) { + // context.read().add( + // AddFunction( + // functionData: DeviceFunctionData( + // entityId: device?.uuid ?? '', + // functionCode: selectCode, + // operationName: operationName, + // condition: conditions[index], + // value: selectedFunctionData?.value ?? selectCode == 'temp_set' + // ? 200 + // : -100, + // valueDescription: selectedFunctionData?.valueDescription, + // ), + // ), + // ); + // }, + // borderRadius: const BorderRadius.all(Radius.circular(8)), + // selectedBorderColor: ColorsManager.primaryColorWithOpacity, + // selectedColor: Colors.white, + // fillColor: ColorsManager.primaryColorWithOpacity, + // color: ColorsManager.primaryColorWithOpacity, + // constraints: const BoxConstraints( + // minHeight: 40.0, + // minWidth: 40.0, + // ), + // isSelected: + // conditions.map((c) => c == (currentCondition ?? "==")).toList(), + // children: conditions.map((c) => Text(c)).toList(), + // ); + // } - /// Build temperature display for AC functions dialog - static Widget _buildTemperatureDisplay( - BuildContext context, - dynamic initialValue, - AllDevicesModel? device, - String operationName, - DeviceFunctionData? selectedFunctionData, - String selectCode, - ) { - final initialVal = selectCode == 'temp_set' ? 200 : -100; - return Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration( - color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - '${(initialValue ?? initialVal) / 10}°C', - style: context.textTheme.headlineMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ), - ); - } + // /// Build temperature display for AC functions dialog + // static Widget _buildTemperatureDisplay( + // BuildContext context, + // dynamic initialValue, + // AllDevicesModel? device, + // String operationName, + // DeviceFunctionData? selectedFunctionData, + // String selectCode, + // ) { + // final initialVal = selectCode == 'temp_set' ? 200 : -100; + // return Container( + // padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + // decoration: BoxDecoration( + // color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), + // borderRadius: BorderRadius.circular(10), + // ), + // child: Text( + // '${(initialValue ?? initialVal) / 10}°C', + // style: context.textTheme.headlineMedium!.copyWith( + // color: ColorsManager.primaryColorWithOpacity, + // ), + // ), + // ); + // } - static Widget _buildTemperatureSlider( - BuildContext context, - dynamic initialValue, - AllDevicesModel? device, - String operationName, - DeviceFunctionData? selectedFunctionData, - String selectCode, - ) { - return Slider( - value: initialValue is int ? initialValue.toDouble() : 200.0, - min: selectCode == 'temp_current' ? -100 : 200, - max: selectCode == 'temp_current' ? 900 : 300, - divisions: 10, - label: '${((initialValue ?? 160) / 10).toInt()}°C', - onChanged: (value) { - context.read().add( - AddFunction( - functionData: DeviceFunctionData( - entityId: device?.uuid ?? '', - functionCode: selectCode, - operationName: operationName, - value: value, - condition: selectedFunctionData?.condition, - valueDescription: selectedFunctionData?.valueDescription, - ), - ), - ); - }, - ); - } + // static Widget _buildTemperatureSlider( + // BuildContext context, + // dynamic initialValue, + // AllDevicesModel? device, + // String operationName, + // DeviceFunctionData? selectedFunctionData, + // String selectCode, + // ) { + // return Slider( + // value: initialValue is int ? initialValue.toDouble() : 200.0, + // min: selectCode == 'temp_current' ? -100 : 200, + // max: selectCode == 'temp_current' ? 900 : 300, + // divisions: 10, + // label: '${((initialValue ?? 160) / 10).toInt()}°C', + // onChanged: (value) { + // context.read().add( + // AddFunction( + // functionData: DeviceFunctionData( + // entityId: device?.uuid ?? '', + // functionCode: selectCode, + // operationName: operationName, + // value: value, + // condition: selectedFunctionData?.condition, + // valueDescription: selectedFunctionData?.valueDescription, + // ), + // ), + // ); + // }, + // ); + // } static Widget _buildOperationalValuesList({ required BuildContext context, @@ -414,7 +450,9 @@ class ACHelper { style: context.textTheme.bodyMedium, ), trailing: Icon( - isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, size: 24, color: isSelected ? ColorsManager.primaryColorWithOpacity @@ -430,7 +468,8 @@ class ACHelper { operationName: operationName, value: value.value, condition: selectedFunctionData?.condition, - valueDescription: selectedFunctionData?.valueDescription, + valueDescription: + selectedFunctionData?.valueDescription, ), ), ); diff --git a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_dialog.dart index f3d07a66..8fab09e8 100644 --- a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/ceiling_sensor_dialog.dart @@ -41,7 +41,8 @@ class _CeilingSensorDialogState extends State { void initState() { super.initState(); - _cpsFunctions = widget.functions.whereType().where((function) { + _cpsFunctions = + widget.functions.whereType().where((function) { if (widget.dialogType == 'THEN') { return function.type == 'THEN' || function.type == 'BOTH'; } @@ -149,6 +150,7 @@ class _CeilingSensorDialogState extends State { device: widget.device, ) : CpsDialogSliderSelector( + step: selectedCpsFunctions.step!, operations: operations, selectedFunction: selectedFunction ?? '', selectedFunctionData: selectedFunctionData, diff --git a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_slider_selector.dart b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_slider_selector.dart index cd8e4c46..f26bd52a 100644 --- a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_slider_selector.dart +++ b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_dialog_slider_selector.dart @@ -4,8 +4,8 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; import 'package:syncrow_web/pages/routines/models/ceiling_presence_sensor_functions.dart'; import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/custom_routines_textbox.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_slider_helpers.dart'; -import 'package:syncrow_web/pages/routines/widgets/slider_value_selector.dart'; class CpsDialogSliderSelector extends StatelessWidget { const CpsDialogSliderSelector({ @@ -16,6 +16,7 @@ class CpsDialogSliderSelector extends StatelessWidget { required this.device, required this.operationName, required this.dialogType, + required this.step, super.key, }); @@ -26,13 +27,16 @@ class CpsDialogSliderSelector extends StatelessWidget { final AllDevicesModel? device; final String operationName; final String dialogType; + final double step; @override Widget build(BuildContext context) { - return SliderValueSelector( + return CustomRoutinesTextbox( + withSpecialChar: true, currentCondition: selectedFunctionData.condition, dialogType: dialogType, - sliderRange: CpsSliderHelpers.sliderRange(selectedFunctionData.functionCode), + sliderRange: + CpsSliderHelpers.sliderRange(selectedFunctionData.functionCode), displayedValue: CpsSliderHelpers.displayText( value: selectedFunctionData.value, functionCode: selectedFunctionData.functionCode, @@ -50,7 +54,7 @@ class CpsDialogSliderSelector extends StatelessWidget { ), ), ), - onSliderChanged: (value) => context.read().add( + onTextChanged: (value) => context.read().add( AddFunction( functionData: DeviceFunctionData( entityId: device?.uuid ?? '', @@ -64,6 +68,7 @@ class CpsDialogSliderSelector extends StatelessWidget { dividendOfRange: CpsSliderHelpers.dividendOfRange( selectedFunctionData.functionCode, ), + stepIncreaseAmount: step, ); } } diff --git a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_functions_list.dart b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_functions_list.dart index efc57653..d11871a7 100644 --- a/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_functions_list.dart +++ b/lib/pages/routines/widgets/routine_dialogs/ceiling_sensor/cps_functions_list.dart @@ -34,30 +34,33 @@ class CpsFunctionsList extends StatelessWidget { itemBuilder: (context, index) { final function = cpsFunctions[index]; return RoutineDialogFunctionListTile( - iconPath: function.icon, - operationName: function.operationName, - onTap: () => RoutineTapFunctionHelper.onTapFunction( - context, - functionCode: function.code, - functionOperationName: function.operationName, - functionValueDescription: selectedFunctionData?.valueDescription, - deviceUuid: device?.uuid, - codesToAddIntoFunctionsWithDefaultValue: [ - 'static_max_dis', - 'presence_reference', - 'moving_reference', - 'perceptual_boundary', - 'moving_boundary', - 'moving_rigger_time', - 'moving_static_time', - 'none_body_time', - 'moving_max_dis', - 'moving_range', - 'presence_range', - if (dialogType == "IF") 'sensitivity', - ], - ), - ); + iconPath: function.icon, + operationName: function.operationName, + onTap: () { + RoutineTapFunctionHelper.onTapFunction( + context, + step: function.step, + functionCode: function.code, + functionOperationName: function.operationName, + functionValueDescription: + selectedFunctionData?.valueDescription, + deviceUuid: device?.uuid, + codesToAddIntoFunctionsWithDefaultValue: [ + 'static_max_dis', + 'presence_reference', + 'moving_reference', + 'perceptual_boundary', + 'moving_boundary', + 'moving_rigger_time', + 'moving_static_time', + 'none_body_time', + 'moving_max_dis', + 'moving_range', + 'presence_range', + if (dialogType == "IF") 'sensitivity', + ], + ); + }); }, ), ); diff --git a/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_operational_values_list.dart b/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_operational_values_list.dart index 1a96cfbb..4c780058 100644 --- a/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_operational_values_list.dart +++ b/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_operational_values_list.dart @@ -1,11 +1,7 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; -import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; -import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/models/flush/flush_operational_value.dart'; -import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/time_wheel.dart'; class FlushOperationalValuesList extends StatelessWidget { final List values; @@ -26,22 +22,20 @@ class FlushOperationalValuesList extends StatelessWidget { @override Widget build(BuildContext context) { - return ListView.builder( - padding: const EdgeInsets.all(20), - itemCount: values.length, - itemBuilder: (context, index) => - _buildValueItem(context, values[index]), - ); + return ListView.builder( + padding: const EdgeInsets.all(20), + itemCount: values.length, + itemBuilder: (context, index) => _buildValueItem(context, values[index]), + ); } - - Widget _buildValueItem(BuildContext context, FlushOperationalValue value) { return Padding( padding: const EdgeInsets.all(8.0), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ + SvgPicture.asset(value.icon, width: 25, height: 25), Expanded(child: _buildValueDescription(value)), _buildValueRadio(context, value), ], @@ -49,9 +43,6 @@ class FlushOperationalValuesList extends StatelessWidget { ); } - - - Widget _buildValueDescription(FlushOperationalValue value) { return Padding( padding: const EdgeInsets.symmetric(horizontal: 8.0), @@ -65,6 +56,4 @@ class FlushOperationalValuesList extends StatelessWidget { groupValue: selectedValue, onChanged: (_) => onSelect(value)); } - - } diff --git a/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart b/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart index bf2146ad..dad64866 100644 --- a/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart +++ b/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_presence_sensor.dart @@ -96,7 +96,9 @@ class _WallPresenceSensorState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const DialogHeader('Presence Sensor Condition'), + DialogHeader(widget.dialogType == 'THEN' + ? 'Presence Sensor Functions' + : 'Presence Sensor Condition'), Expanded(child: _buildMainContent(context, state)), _buildDialogFooter(context, state), ], diff --git a/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_value_selector_widget.dart b/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_value_selector_widget.dart index 64f060e5..7ca89edb 100644 --- a/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_value_selector_widget.dart +++ b/lib/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_value_selector_widget.dart @@ -5,6 +5,7 @@ import 'package:syncrow_web/pages/device_managment/flush_mounted_presence_sensor import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/models/flush/flush_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/custom_routines_textbox.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/flush_presence_sensor/flush_operational_values_list.dart'; import 'package:syncrow_web/pages/routines/widgets/slider_value_selector.dart'; @@ -66,7 +67,8 @@ class FlushValueSelectorWidget extends StatelessWidget { if (isDistanceDetection) { initialValue = initialValue / 100; } - return SliderValueSelector( + return CustomRoutinesTextbox( + withSpecialChar: true, currentCondition: functionData.condition, dialogType: dialogType, sliderRange: sliderRange, @@ -83,7 +85,7 @@ class FlushValueSelectorWidget extends StatelessWidget { ), ), ), - onSliderChanged: (value) { + onTextChanged: (value) { final roundedValue = _roundToStep(value, stepSize); final finalValue = isDistanceDetection ? (roundedValue * 100).toInt() : roundedValue; @@ -102,6 +104,7 @@ class FlushValueSelectorWidget extends StatelessWidget { }, unit: _unit, dividendOfRange: stepSize, + stepIncreaseAmount: stepSize, ); } diff --git a/lib/pages/routines/widgets/routine_dialogs/gateway/gateway_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/gateway/gateway_dialog.dart index 364854ce..33cf8fc0 100644 --- a/lib/pages/routines/widgets/routine_dialogs/gateway/gateway_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/gateway/gateway_dialog.dart @@ -16,9 +16,10 @@ class GatewayDialog extends StatefulWidget { required this.functions, required this.deviceSelectedFunctions, required this.device, + required this.dialogType, super.key, }); - + final String dialogType; final String? uniqueCustomId; final List functions; final List deviceSelectedFunctions; @@ -55,7 +56,9 @@ class _GatewayDialogState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const DialogHeader('Gateway Conditions'), + DialogHeader(widget.dialogType == 'THEN' + ? 'Gateway Functions' + : 'Gateway Conditions'), Expanded(child: _buildMainContent(context, state)), _buildDialogFooter(context, state), ], diff --git a/lib/pages/routines/widgets/routine_dialogs/gateway/gateway_helper.dart b/lib/pages/routines/widgets/routine_dialogs/gateway/gateway_helper.dart index 9a9351ca..19b866c9 100644 --- a/lib/pages/routines/widgets/routine_dialogs/gateway/gateway_helper.dart +++ b/lib/pages/routines/widgets/routine_dialogs/gateway/gateway_helper.dart @@ -14,6 +14,7 @@ abstract final class GatewayHelper { required String? uniqueCustomId, required List deviceSelectedFunctions, required AllDevicesModel? device, + required String dialogType, }) async { return showDialog( context: context, @@ -27,6 +28,7 @@ abstract final class GatewayHelper { functions: functions, deviceSelectedFunctions: deviceSelectedFunctions, device: device, + dialogType:dialogType, ), ), ); diff --git a/lib/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart b/lib/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart index 2b09f579..a3223abd 100644 --- a/lib/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart +++ b/lib/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart @@ -8,6 +8,7 @@ abstract final class RoutineTapFunctionHelper { static void onTapFunction( BuildContext context, { + double? step, required String functionCode, required String functionOperationName, required String? functionValueDescription, diff --git a/lib/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart index 3c786045..c892610c 100644 --- a/lib/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/one_gang_switch_dialog.dart @@ -4,11 +4,11 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; -import 'package:syncrow_web/pages/routines/helper/duration_format_helper.dart'; import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/one_gang_switch/one_gang_switch.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/pages/routines/widgets/custom_routines_textbox.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart'; @@ -59,7 +59,9 @@ class OneGangSwitchHelper { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const DialogHeader('1 Gang Light Switch Condition'), + DialogHeader(dialogType == 'THEN' + ? '1 Gang Light Switch Functions' + : '1 Gang Light Switch Condition'), Expanded( child: Row( children: [ @@ -87,14 +89,15 @@ class OneGangSwitchHelper { size: 16, color: ColorsManager.textGray, ), - onTap: () => - RoutineTapFunctionHelper.onTapFunction( + onTap: () => RoutineTapFunctionHelper + .onTapFunction( context, functionCode: function.code, functionOperationName: function.operationName, functionValueDescription: - selectedFunctionData.valueDescription, + selectedFunctionData + .valueDescription, deviceUuid: device?.uuid, codesToAddIntoFunctionsWithDefaultValue: [ 'countdown_1', @@ -108,14 +111,16 @@ class OneGangSwitchHelper { if (selectedFunction != null) Expanded( child: _buildValueSelector( - context: context, - selectedFunction: selectedFunction, - selectedFunctionData: selectedFunctionData, - acFunctions: oneGangFunctions, - device: device, - operationName: selectedOperationName ?? '', - removeComparetors: removeComparetors, - ), + context: context, + selectedFunction: selectedFunction, + selectedFunctionData: + selectedFunctionData, + acFunctions: oneGangFunctions, + device: device, + operationName: + selectedOperationName ?? '', + removeComparetors: removeComparetors, + dialogType: dialogType), ), ], ), @@ -172,6 +177,7 @@ class OneGangSwitchHelper { AllDevicesModel? device, required String operationName, required bool removeComparetors, + required String dialogType, }) { if (selectedFunction == 'countdown_1') { final initialValue = selectedFunctionData?.value ?? 0; @@ -184,6 +190,7 @@ class OneGangSwitchHelper { operationName: operationName, selectedFunctionData: selectedFunctionData, removeComparetors: removeComparetors, + dialogType: dialogType, ); } final selectedFn = acFunctions.firstWhere( @@ -216,93 +223,18 @@ class OneGangSwitchHelper { required String operationName, DeviceFunctionData? selectedFunctionData, required bool removeComparetors, + String? dialogType, }) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (removeComparetors != true) - _buildConditionToggle( - context, - currentCondition, - selectCode, - device, - operationName, - selectedFunctionData, - ), - const SizedBox(height: 20), - _buildCountDownDisplay(context, initialValue, device, operationName, - selectedFunctionData, selectCode), const SizedBox(height: 20), _buildCountDownSlider(context, initialValue, device, operationName, - selectedFunctionData, selectCode), + selectedFunctionData, selectCode, dialogType!), ], ); } - /// Build condition toggle for AC functions dialog - static Widget _buildConditionToggle( - BuildContext context, - String? currentCondition, - String selectCode, - AllDevicesModel? device, - String operationName, - DeviceFunctionData? selectedFunctionData, - // Function(String) onConditionChanged, - ) { - final conditions = ["<", "==", ">"]; - - return ToggleButtons( - onPressed: (int index) { - context.read().add( - AddFunction( - functionData: DeviceFunctionData( - entityId: device?.uuid ?? '', - functionCode: selectCode, - operationName: operationName, - condition: conditions[index], - value: selectedFunctionData?.value ?? 0, - valueDescription: selectedFunctionData?.valueDescription, - ), - ), - ); - }, - borderRadius: const BorderRadius.all(Radius.circular(8)), - selectedBorderColor: ColorsManager.primaryColorWithOpacity, - selectedColor: Colors.white, - fillColor: ColorsManager.primaryColorWithOpacity, - color: ColorsManager.primaryColorWithOpacity, - constraints: const BoxConstraints( - minHeight: 40.0, - minWidth: 40.0, - ), - isSelected: conditions.map((c) => c == (currentCondition ?? "==")).toList(), - children: conditions.map((c) => Text(c)).toList(), - ); - } - - /// Build temperature display for AC functions dialog - static Widget _buildCountDownDisplay( - BuildContext context, - dynamic initialValue, - AllDevicesModel? device, - String operationName, - DeviceFunctionData? selectedFunctionData, - String selectCode) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration( - color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - DurationFormatMixin.formatDuration(initialValue?.toInt() ?? 0), - style: context.textTheme.headlineMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ), - ); - } - static Widget _buildCountDownSlider( BuildContext context, dynamic initialValue, @@ -310,38 +242,47 @@ class OneGangSwitchHelper { String operationName, DeviceFunctionData? selectedFunctionData, String selectCode, + String dialogType, ) { - const twelveHoursInSeconds = 43200.0; - final operationalValues = SwitchOperationalValue( - icon: '', - description: "sec", - value: 0.0, - minValue: 0, - maxValue: twelveHoursInSeconds, - stepValue: 1, - ); - return Slider( - value: (initialValue ?? 0).toDouble(), - min: operationalValues.minValue?.toDouble() ?? 0.0, - max: operationalValues.maxValue?.toDouble() ?? 0.0, - divisions: - (((operationalValues.maxValue ?? 0) - (operationalValues.minValue ?? 0)) / - (operationalValues.stepValue ?? 1)) - .round(), - onChanged: (value) { + return CustomRoutinesTextbox( + withSpecialChar: false, + currentCondition: selectedFunctionData?.condition, + dialogType: dialogType, + sliderRange: (0, 43200), + displayedValue: (initialValue ?? 0).toString(), + initialValue: (initialValue ?? 0).toString(), + onConditionChanged: (condition) { context.read().add( AddFunction( functionData: DeviceFunctionData( entityId: device?.uuid ?? '', functionCode: selectCode, operationName: operationName, - value: value, + condition: condition, + value: selectedFunctionData?.value ?? 0, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + onTextChanged: (value) { + final roundedValue = value.round(); + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: roundedValue, condition: selectedFunctionData?.condition, valueDescription: selectedFunctionData?.valueDescription, ), ), ); }, + unit: 'sec', + dividendOfRange: 1, + stepIncreaseAmount: 1, ); } @@ -377,7 +318,9 @@ class OneGangSwitchHelper { style: context.textTheme.bodyMedium, ), trailing: Icon( - isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, size: 24, color: isSelected ? ColorsManager.primaryColorWithOpacity @@ -393,7 +336,8 @@ class OneGangSwitchHelper { operationName: operationName, value: value.value, condition: selectedFunctionData?.condition, - valueDescription: selectedFunctionData?.valueDescription, + valueDescription: + selectedFunctionData?.valueDescription, ), ), ); diff --git a/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/enargy_operational_values_list.dart b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/enargy_operational_values_list.dart new file mode 100644 index 00000000..2b8ba68f --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/enargy_operational_values_list.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/models/pc/enrgy_clamp_operational_value.dart'; + +class EnergyOperationalValuesList extends StatelessWidget { + final List values; + final dynamic selectedValue; + final AllDevicesModel? device; + final String operationName; + final String selectCode; + + const EnergyOperationalValuesList({ + required this.values, + required this.selectedValue, + required this.device, + required this.operationName, + required this.selectCode, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + padding: const EdgeInsets.all(20), + itemCount: values.length, + itemBuilder: (context, index) => _buildValueItem(context, values[index]), + ); + } + + Widget _buildValueItem( + BuildContext context, EnergyClampOperationalValue value) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + _buildValueIcon(context, value), + Expanded(child: _buildValueDescription(value)), + _buildValueRadio(context, value), + ], + ), + ); + } + + Widget _buildValueIcon(context, EnergyClampOperationalValue value) { + return Column( + children: [ + SvgPicture.asset(value.icon, width: 25, height: 25), + ], + ); + } + + Widget _buildValueDescription(EnergyClampOperationalValue value) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text(value.description), + ); + } + + Widget _buildValueRadio(context, EnergyClampOperationalValue value) { + return Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (_) => _selectValue(context, value.value), + ); + } + + void _selectValue(BuildContext context, dynamic value) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: value, + ), + ), + ); + } + + +} diff --git a/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart new file mode 100644 index 00000000..b27c5f8a --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_clamp_dialog.dart @@ -0,0 +1,268 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/models/pc/energy_clamp_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_value_selector_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class EnergyClampDialog extends StatefulWidget { + final List functions; + final AllDevicesModel? device; + final List? deviceSelectedFunctions; + final String? uniqueCustomId; + final String? dialogType; + final bool removeComparetors; + + const EnergyClampDialog({ + super.key, + required this.functions, + this.device, + this.deviceSelectedFunctions, + this.uniqueCustomId, + this.dialogType, + this.removeComparetors = false, + }); + + static Future?> showEnergyClampFunctionsDialog({ + required BuildContext context, + required List functions, + AllDevicesModel? device, + List? deviceSelectedFunctions, + String? uniqueCustomId, + String? dialogType, + bool removeComparetors = false, + }) async { + return showDialog?>( + context: context, + builder: (context) => EnergyClampDialog( + functions: functions, + device: device, + deviceSelectedFunctions: deviceSelectedFunctions, + uniqueCustomId: uniqueCustomId, + removeComparetors: removeComparetors, + dialogType: dialogType, + ), + ); + } + + @override + State createState() => _EnergyClampDialogState(); +} + +class _EnergyClampDialogState extends State { + late final List _functions; + + @override + void initState() { + super.initState(); + _functions = + widget.functions.whereType().where((function) { + if (widget.dialogType == 'THEN') { + return function.type == 'THEN' || function.type == 'BOTH'; + } + return function.type == 'IF' || function.type == 'BOTH'; + }).toList(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => FunctionBloc() + ..add(InitializeFunctions(widget.deviceSelectedFunctions ?? [])), + child: _buildDialogContent(), + ); + } + + Widget _buildDialogContent() { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, state) { + final selectedFunction = state.selectedFunction; + return Container( + width: selectedFunction != null ? 600 : 360, + height: 450, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DialogHeader(widget.dialogType == 'THEN' + ? 'Energy Clamp Functions' + : 'Energy Clamp Conditions'), + Expanded( + child: Visibility( + visible: _functions.isNotEmpty, + replacement: SizedBox( + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Center( + child: Text( + 'You Cant add\n the Power Clamp to Then Section', + textAlign: TextAlign.center, + style: context.textTheme.bodyMedium!.copyWith( + color: ColorsManager.red, + fontWeight: FontWeight.w400), + )), + ], + ), + ), + child: _buildMainContent(context, state), + )), + _buildDialogFooter(context, state), + ], + ), + ); + }, + ), + ); + } + + Widget _buildMainContent(BuildContext context, FunctionBlocState state) { + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildFunctionList(context, state), + if (state.selectedFunction != null) _buildValueSelector(context, state), + ], + ); + } + + Widget _buildFunctionList(BuildContext context, FunctionBlocState state) { + final selectedFunction = state.selectedFunction; + final selectedFunctionData = state.addedFunctions.firstWhere( + (f) => f.functionCode == selectedFunction, + orElse: () => DeviceFunctionData( + entityId: '', + functionCode: selectedFunction ?? '', + operationName: '', + value: null, + ), + ); + return SizedBox( + width: 360, + child: ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: _functions.length, + separatorBuilder: (context, index) => const Padding( + padding: EdgeInsets.symmetric(horizontal: 40.0), + child: Divider(color: ColorsManager.dividerColor), + ), + itemBuilder: (context, index) { + final function = _functions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + width: 24, + height: 24, + placeholderBuilder: (context) => const SizedBox( + width: 24, + height: 24, + ), + ), + title: Text( + function.operationName, + style: context.textTheme.bodyMedium, + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), + onTap: () => RoutineTapFunctionHelper.onTapFunction( + context, + functionCode: function.code, + functionOperationName: function.operationName, + functionValueDescription: selectedFunctionData.valueDescription, + deviceUuid: widget.device?.uuid, + codesToAddIntoFunctionsWithDefaultValue: [ + 'VoltageA', + 'CurrentA', + 'ActivePowerA', + 'PowerFactorA', + 'ReactivePowerA', + 'EnergyConsumedA', + 'VoltageB', + 'CurrentB', + 'ActivePowerB', + 'PowerFactorB', + 'ReactivePowerB', + 'EnergyConsumedB', + 'VoltageC', + 'CurrentC', + 'ActivePowerC', + 'PowerFactorC', + 'ReactivePowerC', + 'EnergyConsumedC', + 'EnergyConsumed', + 'Current', + 'ActivePower', + 'ReactivePower', + 'Frequency', + ], + ), + ); + }, + ), + ); + } + + Widget _buildValueSelector(BuildContext context, FunctionBlocState state) { + final selectedFunction = state.selectedFunction!; + final functionData = state.addedFunctions.firstWhere( + (f) => f.functionCode == selectedFunction, + orElse: () => DeviceFunctionData( + entityId: '', + functionCode: selectedFunction, + operationName: state.selectedOperationName ?? '', + value: null, + ), + ); + + return Expanded( + child: EnergyValueSelectorWidget( + selectedFunction: selectedFunction, + functionData: functionData, + functions: _functions, + device: widget.device, + dialogType: widget.dialogType!, + removeComparators: widget.removeComparetors, + ), + ); + } + + Widget _buildDialogFooter(BuildContext context, FunctionBlocState state) { + return DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: state.addedFunctions.isNotEmpty + ? () { + context.read().add( + AddFunctionToRoutine( + state.addedFunctions, + widget.uniqueCustomId!, + ), + ); + Navigator.pop( + context, + {'deviceId': widget.functions.first.deviceId}, + ); + } + : null, + isConfirmEnabled: state.selectedFunction != null, + ); + } +} diff --git a/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_value_selector_widget.dart b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_value_selector_widget.dart new file mode 100644 index 00000000..696251a1 --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/power_clamp_enargy/energy_value_selector_widget.dart @@ -0,0 +1,85 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/models/pc/energy_clamp_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/custom_routines_textbox.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/power_clamp_enargy/enargy_operational_values_list.dart'; + +class EnergyValueSelectorWidget extends StatelessWidget { + final String selectedFunction; + final DeviceFunctionData functionData; + final List functions; + final AllDevicesModel? device; + final String dialogType; + final bool removeComparators; + + const EnergyValueSelectorWidget({ + required this.selectedFunction, + required this.functionData, + required this.functions, + required this.device, + required this.dialogType, + required this.removeComparators, + super.key, + }); + + @override + Widget build(BuildContext context) { + final selectedFn = + functions.firstWhere((f) => f.code == selectedFunction); + final values = selectedFn.getOperationalValues(); + final step = selectedFn.step ?? 1.0; + final _unit = selectedFn.unit ?? ''; + final (double, double) sliderRange = + (selectedFn.min ?? 0.0, selectedFn.max ?? 100.0); + + if (_isSliderFunction(selectedFunction)) { + return CustomRoutinesTextbox( + withSpecialChar: false, + currentCondition: functionData.condition, + dialogType: dialogType, + sliderRange: sliderRange, + displayedValue: functionData.value, + initialValue: functionData.value ?? 0.0, + onConditionChanged: (condition) => context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectedFunction, + operationName: functionData.operationName, + condition: condition, + value: functionData.value ?? 0, + ), + ), + ), + onTextChanged: (value) => context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectedFunction, + operationName: functionData.operationName, + value: value.toInt(), + condition: functionData.condition, + ), + ), + ), + unit: _unit, + dividendOfRange: 1, + stepIncreaseAmount: step, + ); + } + + return EnergyOperationalValuesList( + values: values, + selectedValue: functionData.value, + device: device, + operationName: selectedFn.operationName, + selectCode: selectedFunction, + ); + } + + bool _isSliderFunction(String function) => + !['voltage_phase_seq'].contains(function); +} diff --git a/lib/pages/routines/widgets/routine_dialogs/three_gang_switch_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/three_gang_switch_dialog.dart index 44a367ef..00c3165a 100644 --- a/lib/pages/routines/widgets/routine_dialogs/three_gang_switch_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/three_gang_switch_dialog.dart @@ -4,10 +4,10 @@ import 'package:flutter_svg/flutter_svg.dart'; import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; -import 'package:syncrow_web/pages/routines/helper/duration_format_helper.dart'; import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/pages/routines/widgets/custom_routines_textbox.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart'; @@ -58,7 +58,9 @@ class ThreeGangSwitchHelper { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const DialogHeader('3 Gangs Light Switch Condition'), + DialogHeader(dialogType == 'THEN' + ? '3 Gangs Light Switch Functions' + : '3 Gangs Light Switch Condition'), Expanded( child: Row( children: [ @@ -86,20 +88,21 @@ class ThreeGangSwitchHelper { size: 16, color: ColorsManager.textGray, ), - onTap: () => - RoutineTapFunctionHelper.onTapFunction( + onTap: () => RoutineTapFunctionHelper + .onTapFunction( context, functionCode: function.code, functionOperationName: function.operationName, functionValueDescription: - selectedFunctionData.valueDescription, + selectedFunctionData + .valueDescription, deviceUuid: device?.uuid, - codesToAddIntoFunctionsWithDefaultValue: [ - 'countdown_1', - 'countdown_2', - 'countdown_3', - ], + codesToAddIntoFunctionsWithDefaultValue: + function.code + .startsWith('countdown') + ? [function.code] + : [], ), ); }, @@ -109,14 +112,16 @@ class ThreeGangSwitchHelper { if (selectedFunction != null) Expanded( child: _buildValueSelector( - context: context, - selectedFunction: selectedFunction, - selectedFunctionData: selectedFunctionData, - switchFunctions: switchFunctions, - device: device, - operationName: selectedOperationName ?? '', - removeComparetors: removeComparetors, - ), + context: context, + selectedFunction: selectedFunction, + selectedFunctionData: + selectedFunctionData, + switchFunctions: switchFunctions, + device: device, + operationName: + selectedOperationName ?? '', + removeComparetors: removeComparetors, + dialogType: dialogType), ), ], ), @@ -133,14 +138,6 @@ class ThreeGangSwitchHelper { onConfirm: state.addedFunctions.isNotEmpty ? () { /// add the functions to the routine bloc - // for (var function in state.addedFunctions) { - // context.read().add( - // AddFunctionToRoutine( - // function, - // uniqueCustomId, - // ), - // ); - // } context.read().add( AddFunctionToRoutine( state.addedFunctions, @@ -173,24 +170,26 @@ class ThreeGangSwitchHelper { AllDevicesModel? device, required String operationName, required bool removeComparetors, + required String dialogType, }) { if (selectedFunction == 'countdown_1' || selectedFunction == 'countdown_2' || selectedFunction == 'countdown_3') { final initialValue = selectedFunctionData?.value ?? 0; return _buildTemperatureSelector( - context: context, - initialValue: initialValue, - selectCode: selectedFunction, - currentCondition: selectedFunctionData?.condition, - device: device, - operationName: operationName, - selectedFunctionData: selectedFunctionData, - removeComparetors: removeComparetors, - ); + context: context, + initialValue: initialValue, + selectCode: selectedFunction, + currentCondition: selectedFunctionData?.condition, + device: device, + operationName: operationName, + selectedFunctionData: selectedFunctionData, + removeComparetors: removeComparetors, + dialogType: dialogType); } - final selectedFn = switchFunctions.firstWhere((f) => f.code == selectedFunction); + final selectedFn = + switchFunctions.firstWhere((f) => f.code == selectedFunction); final values = selectedFn.getOperationalValues(); return _buildOperationalValuesList( @@ -213,93 +212,18 @@ class ThreeGangSwitchHelper { required String operationName, DeviceFunctionData? selectedFunctionData, bool? removeComparetors, + required String dialogType, }) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (removeComparetors != true) - _buildConditionToggle( - context, - currentCondition, - selectCode, - device, - operationName, - selectedFunctionData, - ), - const SizedBox(height: 20), - _buildCountDownDisplay(context, initialValue, device, operationName, - selectedFunctionData, selectCode), const SizedBox(height: 20), _buildCountDownSlider(context, initialValue, device, operationName, - selectedFunctionData, selectCode), + selectedFunctionData, selectCode, dialogType), ], ); } - /// Build condition toggle for AC functions dialog - static Widget _buildConditionToggle( - BuildContext context, - String? currentCondition, - String selectCode, - AllDevicesModel? device, - String operationName, - DeviceFunctionData? selectedFunctionData, - // Function(String) onConditionChanged, - ) { - final conditions = ["<", "==", ">"]; - - return ToggleButtons( - onPressed: (int index) { - context.read().add( - AddFunction( - functionData: DeviceFunctionData( - entityId: device?.uuid ?? '', - functionCode: selectCode, - operationName: operationName, - condition: conditions[index], - value: selectedFunctionData?.value ?? 0, - valueDescription: selectedFunctionData?.valueDescription, - ), - ), - ); - }, - borderRadius: const BorderRadius.all(Radius.circular(8)), - selectedBorderColor: ColorsManager.primaryColorWithOpacity, - selectedColor: Colors.white, - fillColor: ColorsManager.primaryColorWithOpacity, - color: ColorsManager.primaryColorWithOpacity, - constraints: const BoxConstraints( - minHeight: 40.0, - minWidth: 40.0, - ), - isSelected: conditions.map((c) => c == (currentCondition ?? "==")).toList(), - children: conditions.map((c) => Text(c)).toList(), - ); - } - - /// Build temperature display for AC functions dialog - static Widget _buildCountDownDisplay( - BuildContext context, - dynamic initialValue, - AllDevicesModel? device, - String operationName, - DeviceFunctionData? selectedFunctionData, - String selectCode) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration( - color: ColorsManager.primaryColorWithOpacity.withOpacity(0.1), - borderRadius: BorderRadius.circular(10), - ), - child: Text( - DurationFormatMixin.formatDuration(initialValue?.toInt() ?? 0), - style: context.textTheme.headlineMedium!.copyWith( - color: ColorsManager.primaryColorWithOpacity, - ), - ), - ); - } - static Widget _buildCountDownSlider( BuildContext context, dynamic initialValue, @@ -307,38 +231,47 @@ class ThreeGangSwitchHelper { String operationName, DeviceFunctionData? selectedFunctionData, String selectCode, + String dialogType, ) { - const twelveHoursInSeconds = 43200.0; - final operationalValues = SwitchOperationalValue( - icon: '', - description: "sec", - value: 0.0, - minValue: 0, - maxValue: twelveHoursInSeconds, - stepValue: 1, - ); - return Slider( - value: (initialValue ?? 0).toDouble(), - min: operationalValues.minValue?.toDouble() ?? 0.0, - max: operationalValues.maxValue?.toDouble() ?? 0.0, - divisions: - (((operationalValues.maxValue ?? 0) - (operationalValues.minValue ?? 0)) / - (operationalValues.stepValue ?? 1)) - .round(), - onChanged: (value) { + return CustomRoutinesTextbox( + withSpecialChar: true, + currentCondition: selectedFunctionData?.condition, + dialogType: dialogType, + sliderRange: (0, 43200), + displayedValue: (initialValue ?? 0).toString(), + initialValue: (initialValue ?? 0).toString(), + onConditionChanged: (condition) { context.read().add( AddFunction( functionData: DeviceFunctionData( entityId: device?.uuid ?? '', functionCode: selectCode, operationName: operationName, - value: value, + condition: condition, + value: selectedFunctionData?.value ?? 0, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + onTextChanged: (value) { + final roundedValue = value.round(); + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: roundedValue, condition: selectedFunctionData?.condition, valueDescription: selectedFunctionData?.valueDescription, ), ), ); }, + unit: 'sec', + dividendOfRange: 1, + stepIncreaseAmount: 1, ); } @@ -374,7 +307,9 @@ class ThreeGangSwitchHelper { style: context.textTheme.bodyMedium, ), trailing: Icon( - isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, size: 24, color: isSelected ? ColorsManager.primaryColorWithOpacity @@ -390,7 +325,8 @@ class ThreeGangSwitchHelper { operationName: operationName, value: value.value, condition: selectedFunctionData?.condition, - valueDescription: selectedFunctionData?.valueDescription, + valueDescription: + selectedFunctionData?.valueDescription, ), ), ); diff --git a/lib/pages/routines/widgets/routine_dialogs/two_gang_switch_dialog.dart b/lib/pages/routines/widgets/routine_dialogs/two_gang_switch_dialog.dart index f551d21b..93ab83a7 100644 --- a/lib/pages/routines/widgets/routine_dialogs/two_gang_switch_dialog.dart +++ b/lib/pages/routines/widgets/routine_dialogs/two_gang_switch_dialog.dart @@ -8,6 +8,7 @@ import 'package:syncrow_web/pages/routines/helper/duration_format_helper.dart'; import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/base_switch_function.dart'; import 'package:syncrow_web/pages/routines/models/gang_switches/switch_operational_value.dart'; +import 'package:syncrow_web/pages/routines/widgets/custom_routines_textbox.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/helpers/routine_tap_function_helper.dart'; @@ -58,7 +59,9 @@ class TwoGangSwitchHelper { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const DialogHeader('2 Gangs Light Switch Condition'), + DialogHeader(dialogType == 'THEN' + ? '2 Gangs Light Switch Functions' + : '2 Gangs Light Switch Condition'), Expanded( child: Row( children: [ @@ -86,14 +89,15 @@ class TwoGangSwitchHelper { size: 16, color: ColorsManager.textGray, ), - onTap: () => - RoutineTapFunctionHelper.onTapFunction( + onTap: () => RoutineTapFunctionHelper + .onTapFunction( context, functionCode: function.code, functionOperationName: function.operationName, functionValueDescription: - selectedFunctionData.valueDescription, + selectedFunctionData + .valueDescription, deviceUuid: device?.uuid, codesToAddIntoFunctionsWithDefaultValue: [ 'countdown_1', @@ -115,6 +119,7 @@ class TwoGangSwitchHelper { device: device, operationName: selectedOperationName ?? '', removeComparetors: removeComparetors, + dialogType: dialogType, ), ), ], @@ -172,22 +177,25 @@ class TwoGangSwitchHelper { AllDevicesModel? device, required String operationName, required bool removeComparetors, + required String dialogType, }) { - if (selectedFunction == 'countdown_1' || selectedFunction == 'countdown_2') { + if (selectedFunction == 'countdown_1' || + selectedFunction == 'countdown_2') { final initialValue = selectedFunctionData?.value ?? 0; return _buildTemperatureSelector( - context: context, - initialValue: initialValue, - selectCode: selectedFunction, - currentCondition: selectedFunctionData?.condition, - device: device, - operationName: operationName, - selectedFunctionData: selectedFunctionData, - removeComparetors: removeComparetors, - ); + context: context, + initialValue: initialValue, + selectCode: selectedFunction, + currentCondition: selectedFunctionData?.condition, + device: device, + operationName: operationName, + selectedFunctionData: selectedFunctionData, + removeComparetors: removeComparetors, + dialogType: dialogType); } - final selectedFn = switchFunctions.firstWhere((f) => f.code == selectedFunction); + final selectedFn = + switchFunctions.firstWhere((f) => f.code == selectedFunction); final values = selectedFn.getOperationalValues(); return _buildOperationalValuesList( @@ -210,25 +218,13 @@ class TwoGangSwitchHelper { required String operationName, DeviceFunctionData? selectedFunctionData, bool? removeComparetors, + String? dialogType, }) { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - if (removeComparetors != true) - _buildConditionToggle( - context, - currentCondition, - selectCode, - device, - operationName, - selectedFunctionData, - ), - const SizedBox(height: 20), - _buildCountDownDisplay(context, initialValue, device, operationName, - selectedFunctionData, selectCode), - const SizedBox(height: 20), _buildCountDownSlider(context, initialValue, device, operationName, - selectedFunctionData, selectCode), + selectedFunctionData, selectCode, dialogType!), ], ); } @@ -269,7 +265,8 @@ class TwoGangSwitchHelper { minHeight: 40.0, minWidth: 40.0, ), - isSelected: conditions.map((c) => c == (currentCondition ?? "==")).toList(), + isSelected: + conditions.map((c) => c == (currentCondition ?? "==")).toList(), children: conditions.map((c) => Text(c)).toList(), ); } @@ -304,38 +301,48 @@ class TwoGangSwitchHelper { String operationName, DeviceFunctionData? selectedFunctionData, String selectCode, + String dialogType, ) { - const twelveHoursInSeconds = 43200.0; - final operationalValues = SwitchOperationalValue( - icon: '', - description: "sec", - value: 0.0, - minValue: 0, - maxValue: twelveHoursInSeconds, - stepValue: 1, - ); - return Slider( - value: (initialValue ?? 0).toDouble(), - min: operationalValues.minValue?.toDouble() ?? 0.0, - max: operationalValues.maxValue?.toDouble() ?? 0.0, - divisions: - (((operationalValues.maxValue ?? 0) - (operationalValues.minValue ?? 0)) / - (operationalValues.stepValue ?? 1)) - .round(), - onChanged: (value) { + return CustomRoutinesTextbox( + withSpecialChar: true, + currentCondition: selectedFunctionData?.condition, + dialogType: dialogType, + sliderRange: (0, 43200), + displayedValue: (initialValue ?? 0).toString(), + initialValue: (initialValue ?? 0).toString(), + onConditionChanged: (condition) { context.read().add( AddFunction( functionData: DeviceFunctionData( entityId: device?.uuid ?? '', functionCode: selectCode, operationName: operationName, - value: value, + condition: condition, + value: selectedFunctionData?.value ?? 0, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + onTextChanged: (value) { + final roundedValue = + value.round(); // Round to nearest integer (stepSize 1) + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: roundedValue, condition: selectedFunctionData?.condition, valueDescription: selectedFunctionData?.valueDescription, ), ), ); }, + unit: 'sec', + dividendOfRange: 1, + stepIncreaseAmount: 1, ); } @@ -371,7 +378,9 @@ class TwoGangSwitchHelper { style: context.textTheme.bodyMedium, ), trailing: Icon( - isSelected ? Icons.radio_button_checked : Icons.radio_button_unchecked, + isSelected + ? Icons.radio_button_checked + : Icons.radio_button_unchecked, size: 24, color: isSelected ? ColorsManager.primaryColorWithOpacity @@ -387,7 +396,8 @@ class TwoGangSwitchHelper { operationName: operationName, value: value.value, condition: selectedFunctionData?.condition, - valueDescription: selectedFunctionData?.valueDescription, + valueDescription: + selectedFunctionData?.valueDescription, ), ), ); diff --git a/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wall_presence_sensor.dart b/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wall_presence_sensor.dart index 4d04102d..996e46a8 100644 --- a/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wall_presence_sensor.dart +++ b/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wall_presence_sensor.dart @@ -63,7 +63,8 @@ class _WallPresenceSensorState extends State { @override void initState() { super.initState(); - _wpsFunctions = widget.functions.whereType().where((function) { + _wpsFunctions = + widget.functions.whereType().where((function) { if (widget.dialogType == 'THEN') { return function.type == 'THEN' || function.type == 'BOTH'; } @@ -97,7 +98,9 @@ class _WallPresenceSensorState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const DialogHeader('Presence Sensor Condition'), + DialogHeader(widget.dialogType == 'THEN' + ? 'Presence Sensor Functions' + : 'Presence Sensor Condition'), Expanded(child: _buildMainContent(context, state)), _buildDialogFooter(context, state), ], diff --git a/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart b/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart index 30232846..61a7959b 100644 --- a/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart +++ b/lib/pages/routines/widgets/routine_dialogs/wall_sensor/wps_value_selector_widget.dart @@ -4,8 +4,8 @@ import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_mo import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; import 'package:syncrow_web/pages/routines/models/device_functions.dart'; import 'package:syncrow_web/pages/routines/models/wps/wps_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/custom_routines_textbox.dart'; import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/wall_sensor/wps_operational_values_list.dart'; -import 'package:syncrow_web/pages/routines/widgets/slider_value_selector.dart'; class WpsValueSelectorWidget extends StatelessWidget { final String selectedFunction; @@ -27,11 +27,13 @@ class WpsValueSelectorWidget extends StatelessWidget { @override Widget build(BuildContext context) { - final selectedFn = wpsFunctions.firstWhere((f) => f.code == selectedFunction); + final selectedFn = + wpsFunctions.firstWhere((f) => f.code == selectedFunction); final values = selectedFn.getOperationalValues(); if (_isSliderFunction(selectedFunction)) { - return SliderValueSelector( + return CustomRoutinesTextbox( + withSpecialChar: true, currentCondition: functionData.condition, dialogType: dialogType, sliderRange: sliderRange, @@ -48,7 +50,7 @@ class WpsValueSelectorWidget extends StatelessWidget { ), ), ), - onSliderChanged: (value) => context.read().add( + onTextChanged: (value) => context.read().add( AddFunction( functionData: DeviceFunctionData( entityId: device?.uuid ?? '', @@ -61,6 +63,7 @@ class WpsValueSelectorWidget extends StatelessWidget { ), unit: _unit, dividendOfRange: 1, + stepIncreaseAmount: _steps, ); } @@ -99,4 +102,10 @@ class WpsValueSelectorWidget extends StatelessWidget { 'illuminance_value' => 'Lux', _ => '', }; + double get _steps => switch (functionData.functionCode) { + 'presence_time' => 1, + 'dis_current' => 1, + 'illuminance_value' => 1, + _ => 1, + }; } diff --git a/lib/pages/routines/widgets/routine_dialogs/water_heater/water_heater_operational_values_list.dart b/lib/pages/routines/widgets/routine_dialogs/water_heater/water_heater_operational_values_list.dart new file mode 100644 index 00000000..4042df36 --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/water_heater/water_heater_operational_values_list.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/models/water_heater/water_heater_operational_value.dart'; + +class WaterHeaterOperationalValuesList extends StatelessWidget { + final List values; + final dynamic selectedValue; + final AllDevicesModel? device; + final String operationName; + final String selectCode; + final ValueChanged onSelect; + const WaterHeaterOperationalValuesList({ + required this.values, + required this.selectedValue, + required this.device, + required this.operationName, + required this.selectCode, + required this.onSelect, + super.key, + }); + + @override + Widget build(BuildContext context) { + return ListView.builder( + padding: const EdgeInsets.all(20), + itemCount: values.length, + itemBuilder: (context, index) => _buildValueItem(context, values[index]), + ); + } + + Widget _buildValueItem( + BuildContext context, WaterHeaterOperationalValue value) { + return Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SvgPicture.asset( + value.icon, + width: 24, + height: 24, + ), + Expanded(child: _buildValueDescription(value)), + _buildValueRadio(context, value), + ], + ), + ); + } + + Widget _buildValueDescription(WaterHeaterOperationalValue value) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text(value.description), + ); + } + + Widget _buildValueRadio(context, WaterHeaterOperationalValue value) { + return Radio( + value: value.value, + groupValue: selectedValue, + onChanged: (_) => onSelect(value)); + } + +} diff --git a/lib/pages/routines/widgets/routine_dialogs/water_heater/water_heater_presence_sensor.dart b/lib/pages/routines/widgets/routine_dialogs/water_heater/water_heater_presence_sensor.dart new file mode 100644 index 00000000..a4f14aa9 --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/water_heater/water_heater_presence_sensor.dart @@ -0,0 +1,206 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/flutter_svg.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routines/bloc/routine_bloc/routine_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/models/water_heater/water_heater_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/dialog_footer.dart'; +import 'package:syncrow_web/pages/routines/widgets/dialog_header.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/water_heater/water_heater_value_selector_widget.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +class WaterHeaterDialogRoutines extends StatefulWidget { + final List functions; + final AllDevicesModel? device; + final List? deviceSelectedFunctions; + final String? uniqueCustomId; + final String dialogType; + + const WaterHeaterDialogRoutines({ + super.key, + required this.functions, + this.device, + this.deviceSelectedFunctions, + this.uniqueCustomId, + required this.dialogType, + }); + + static Future?> showWHFunctionsDialog({ + required BuildContext context, + required List functions, + AllDevicesModel? device, + List? deviceSelectedFunctions, + String? uniqueCustomId, + required String dialogType, + }) async { + return showDialog?>( + context: context, + builder: (context) => WaterHeaterDialogRoutines( + functions: functions, + device: device, + deviceSelectedFunctions: deviceSelectedFunctions, + uniqueCustomId: uniqueCustomId, + dialogType: dialogType, + ), + ); + } + + @override + State createState() => + _WaterHeaterDialogRoutinesState(); +} + +class _WaterHeaterDialogRoutinesState extends State { + late final List _waterHeaterFunctions; + @override + void initState() { + super.initState(); + _waterHeaterFunctions = + widget.functions.whereType().where((function) { + if (widget.dialogType == 'THEN') { + return function.type == 'THEN' || function.type == 'BOTH'; + } + return function.type == 'IF' || function.type == 'BOTH'; + }).toList(); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => FunctionBloc() + ..add(InitializeFunctions(widget.deviceSelectedFunctions ?? [])), + child: _buildDialogContent(), + ); + } + + Widget _buildDialogContent() { + return AlertDialog( + contentPadding: EdgeInsets.zero, + content: BlocBuilder( + builder: (context, state) { + final selectedFunction = state.selectedFunction; + return Container( + width: selectedFunction != null ? 600 : 360, + height: 450, + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.only(top: 20), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + DialogHeader(widget.dialogType == 'THEN' + ? 'Water Heater Funtions' + : 'Water Heater Condition'), + Expanded(child: _buildMainContent(context, state)), + _buildDialogFooter(context, state), + ], + ), + ); + }, + ), + ); + } + + Widget _buildMainContent(BuildContext context, FunctionBlocState state) { + return Row( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildFunctionList(context), + if (state.selectedFunction != null) _buildValueSelector(context, state), + ], + ); + } + + Widget _buildFunctionList(BuildContext context) { + return SizedBox( + width: 360, + child: ListView.separated( + shrinkWrap: false, + physics: const AlwaysScrollableScrollPhysics(), + itemCount: _waterHeaterFunctions.length, + separatorBuilder: (context, index) => const Padding( + padding: EdgeInsets.symmetric(horizontal: 40.0), + child: Divider(color: ColorsManager.dividerColor), + ), + itemBuilder: (context, index) { + final function = _waterHeaterFunctions[index]; + return ListTile( + leading: SvgPicture.asset( + function.icon, + width: 24, + height: 24, + placeholderBuilder: (context) => const SizedBox( + width: 24, + height: 24, + ), + ), + title: Text( + function.operationName, + style: context.textTheme.bodyMedium, + ), + trailing: const Icon( + Icons.arrow_forward_ios, + size: 16, + color: ColorsManager.textGray, + ), + onTap: () => context.read().add( + SelectFunction( + functionCode: function.code, + operationName: function.operationName, + ), + ), + ); + }, + ), + ); + } + + Widget _buildValueSelector(BuildContext context, FunctionBlocState state) { + final selectedFunction = state.selectedFunction ?? ''; + final functionData = state.addedFunctions.firstWhere( + (f) => f.functionCode == selectedFunction, + orElse: () => DeviceFunctionData( + entityId: '', + functionCode: selectedFunction, + operationName: state.selectedOperationName ?? '', + value: null, + ), + ); + + return Expanded( + child: WaterHeaterValueSelectorWidget( + selectedFunction: selectedFunction, + functionData: functionData, + whFunctions: _waterHeaterFunctions, + device: widget.device, + dialogType: widget.dialogType, + ), + ); + } + + Widget _buildDialogFooter(BuildContext context, FunctionBlocState state) { + return DialogFooter( + onCancel: () => Navigator.pop(context), + onConfirm: state.addedFunctions.isNotEmpty + ? () { + context.read().add( + AddFunctionToRoutine( + state.addedFunctions, + widget.uniqueCustomId!, + ), + ); + Navigator.pop( + context, + {'deviceId': widget.functions.first.deviceId}, + ); + } + : null, + isConfirmEnabled: state.selectedFunction != null, + ); + } +} diff --git a/lib/pages/routines/widgets/routine_dialogs/water_heater/water_heater_value_selector_widget.dart b/lib/pages/routines/widgets/routine_dialogs/water_heater/water_heater_value_selector_widget.dart new file mode 100644 index 00000000..a09bbba7 --- /dev/null +++ b/lib/pages/routines/widgets/routine_dialogs/water_heater/water_heater_value_selector_widget.dart @@ -0,0 +1,127 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/device_managment/all_devices/models/devices_model.dart'; +import 'package:syncrow_web/pages/routines/bloc/functions_bloc/functions_bloc_bloc.dart'; +import 'package:syncrow_web/pages/routines/models/device_functions.dart'; +import 'package:syncrow_web/pages/routines/models/water_heater/water_heater_functions.dart'; +import 'package:syncrow_web/pages/routines/widgets/custom_routines_textbox.dart'; +import 'package:syncrow_web/pages/routines/widgets/routine_dialogs/water_heater/water_heater_operational_values_list.dart'; + +class WaterHeaterValueSelectorWidget extends StatelessWidget { + final String selectedFunction; + final DeviceFunctionData functionData; + final List whFunctions; + final AllDevicesModel? device; + final String dialogType; + + const WaterHeaterValueSelectorWidget({ + required this.selectedFunction, + required this.functionData, + required this.whFunctions, + required this.device, + required this.dialogType, + super.key, + }); + + @override + Widget build(BuildContext context) { + final selectedFn = whFunctions.firstWhere( + (f) => f.code == selectedFunction, + orElse: () => WHSwitchFunction( + deviceId: '', + deviceName: '', + type: '', + ), + ); + if (selectedFunction == 'countdown_1') { + return Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildCountDownSlider( + context, + functionData.value, + device, + selectedFn.operationName, + functionData, + selectedFunction, + dialogType + ), + const SizedBox(height: 10), + ], + ); + } + + return WaterHeaterOperationalValuesList( + values: selectedFn.getOperationalValues(), + selectedValue: functionData.value, + device: device, + operationName: selectedFn.operationName, + selectCode: selectedFunction, + onSelect: (selectedValue) async { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectedFunction, + operationName: functionData.operationName, + value: selectedValue.value, + condition: functionData.condition, + ), + ), + ); + }, + ); + } + + static Widget _buildCountDownSlider( + BuildContext context, + dynamic initialValue, + AllDevicesModel? device, + String operationName, + DeviceFunctionData? selectedFunctionData, + String selectCode, + String dialogType, + ) { + return CustomRoutinesTextbox( + withSpecialChar: false, + currentCondition: selectedFunctionData?.condition, + dialogType: dialogType, + sliderRange: (0, 43200), + displayedValue: (initialValue ?? 0).toString(), + initialValue: (initialValue ?? 0).toString(), + onConditionChanged: (condition) { + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: condition, + condition: condition, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + onTextChanged: (value) { + final roundedValue = value.round(); + context.read().add( + AddFunction( + functionData: DeviceFunctionData( + entityId: device?.uuid ?? '', + functionCode: selectCode, + operationName: operationName, + value: roundedValue, + condition: selectedFunctionData?.condition, + valueDescription: selectedFunctionData?.valueDescription, + ), + ), + ); + }, + unit: 'sec', + dividendOfRange: 1, + stepIncreaseAmount: 1, + ); + } +} diff --git a/lib/pages/routines/widgets/then_container.dart b/lib/pages/routines/widgets/then_container.dart index 1e7e1382..d9eee4c4 100644 --- a/lib/pages/routines/widgets/then_container.dart +++ b/lib/pages/routines/widgets/then_container.dart @@ -116,7 +116,8 @@ class ThenContainer extends StatelessWidget { 'WPS', 'CPS', "GW", - "NCPS" + "NCPS", + 'WH', ].contains(state.thenItems[index] ['productType'])) { context.read().add( @@ -232,8 +233,18 @@ class ThenContainer extends StatelessWidget { dialogType: "THEN"); if (result != null) { context.read().add(AddToThenContainer(mutableData)); - } else if (!['AC', '1G', '2G', '3G', 'WPS', 'GW', 'CPS', "NCPS"] - .contains(mutableData['productType'])) { + } else if (![ + 'AC', + '1G', + '2G', + '3G', + 'WPS', + 'GW', + 'CPS', + "NCPS", + "WH", + 'PC', + ].contains(mutableData['productType'])) { context.read().add(AddToThenContainer(mutableData)); } }, diff --git a/lib/pages/space_tree/bloc/space_tree_bloc.dart b/lib/pages/space_tree/bloc/space_tree_bloc.dart index a3a29004..e8c2e015 100644 --- a/lib/pages/space_tree/bloc/space_tree_bloc.dart +++ b/lib/pages/space_tree/bloc/space_tree_bloc.dart @@ -24,6 +24,9 @@ class SpaceTreeBloc extends Bloc { on(_fetchPaginationSpaces); on(_onDebouncedSearch); on(_onSpaceTreeClearSelectionEvent); + on( + _onAnalyticsClearAllSpaceTreeSelectionsEvent, + ); } Timer _timer = Timer(const Duration(microseconds: 0), () {}); @@ -493,6 +496,20 @@ class SpaceTreeBloc extends Bloc { ); } + void _onAnalyticsClearAllSpaceTreeSelectionsEvent( + AnalyticsClearAllSpaceTreeSelectionsEvent event, + Emitter emit, + ) async { + emit( + state.copyWith( + selectedCommunities: [], + selectedCommunityAndSpaces: {}, + selectedSpaces: [], + soldCheck: [], + ), + ); + } + @override Future close() async { _timer.cancel(); diff --git a/lib/pages/space_tree/bloc/space_tree_event.dart b/lib/pages/space_tree/bloc/space_tree_event.dart index 9c2342fc..6e1687af 100644 --- a/lib/pages/space_tree/bloc/space_tree_event.dart +++ b/lib/pages/space_tree/bloc/space_tree_event.dart @@ -112,3 +112,7 @@ class ClearCachedData extends SpaceTreeEvent {} class SpaceTreeClearSelectionEvent extends SpaceTreeEvent { const SpaceTreeClearSelectionEvent(); } + +final class AnalyticsClearAllSpaceTreeSelectionsEvent extends SpaceTreeEvent { + const AnalyticsClearAllSpaceTreeSelectionsEvent(); +} diff --git a/lib/pages/space_tree/view/space_tree_view.dart b/lib/pages/space_tree/view/space_tree_view.dart index fadcdc0c..c60474f8 100644 --- a/lib/pages/space_tree/view/space_tree_view.dart +++ b/lib/pages/space_tree/view/space_tree_view.dart @@ -48,7 +48,8 @@ class _SpaceTreeViewState extends State { @override Widget build(BuildContext context) { - return BlocBuilder(builder: (context, state) { + return BlocBuilder( + builder: (context, state) { final communities = state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList; @@ -132,104 +133,118 @@ class _SpaceTreeViewState extends State { ) else CustomSearchBar( - onSearchChanged: (query) => context.read().add( - SearchQueryEvent(query), - ), + onSearchChanged: (query) => + context.read().add( + SearchQueryEvent(query), + ), ), const SizedBox(height: 16), Expanded( child: state.isSearching ? const Center(child: CircularProgressIndicator()) - : SidebarCommunitiesList( - onScrollToEnd: () { - if (!state.paginationIsLoading) { - context.read().add( - PaginationEvent( - state.paginationModel, - state.communityList, - ), - ); - } - }, - scrollController: _scrollController, - communities: communities, - itemBuilder: (context, index) { - return CustomExpansionTileSpaceTree( - title: communities[index].name, - isSelected: state.selectedCommunities - .contains(communities[index].uuid), - isSoldCheck: state.selectedCommunities - .contains(communities[index].uuid), - onExpansionChanged: () => - context.read().add( - OnCommunityExpanded( - communities[index].uuid, - ), - ), - isExpanded: state.expandedCommunities.contains( - communities[index].uuid, + : communities.isEmpty + ? Center( + child: Text( + 'No communities found', + style: context.textTheme.bodyMedium?.copyWith( + color: ColorsManager.textGray, + ), ), - onItemSelected: () { - widget.onSelect(); - context.read().add( - OnCommunitySelected( - communities[index].uuid, - communities[index].spaces, - ), - ); - }, - children: communities[index].spaces.map( - (space) { - return CustomExpansionTileSpaceTree( - title: space.name, - isExpanded: - state.expandedSpaces.contains(space.uuid), - onItemSelected: () { - final isParentSelected = _isParentSelected( - state, - communities[index], - space, + ) + : SidebarCommunitiesList( + onScrollToEnd: () { + if (!state.paginationIsLoading) { + context.read().add( + PaginationEvent( + state.paginationModel, + state.communityList, + ), ); - if (widget - .shouldDisableDeselectingChildrenOfSelectedParent && - isParentSelected) { - return; - } - widget.onSelect(); + } + }, + scrollController: _scrollController, + communities: communities, + itemBuilder: (context, index) { + return CustomExpansionTileSpaceTree( + title: communities[index].name, + isSelected: state.selectedCommunities + .contains(communities[index].uuid), + isSoldCheck: state.selectedCommunities + .contains(communities[index].uuid), + onExpansionChanged: () => context.read().add( - OnSpaceSelected( - communities[index], - space.uuid ?? '', - space.children, + OnCommunityExpanded( + communities[index].uuid, ), + ), + isExpanded: + state.expandedCommunities.contains( + communities[index].uuid, + ), + onItemSelected: () { + widget.onSelect(); + context.read().add( + OnCommunitySelected( + communities[index].uuid, + communities[index].spaces, + ), + ); + }, + children: communities[index].spaces.map( + (space) { + return CustomExpansionTileSpaceTree( + title: space.name, + isExpanded: state.expandedSpaces + .contains(space.uuid), + onItemSelected: () { + final isParentSelected = + _isParentSelected( + state, + communities[index], + space, ); + if (widget + .shouldDisableDeselectingChildrenOfSelectedParent && + isParentSelected) { + return; + } + widget.onSelect(); + context.read().add( + OnSpaceSelected( + communities[index], + space.uuid ?? '', + space.children, + ), + ); + }, + onExpansionChanged: () => + context.read().add( + OnSpaceExpanded( + communities[index].uuid, + space.uuid ?? '', + ), + ), + isSelected: state.selectedSpaces + .contains(space.uuid) || + state.soldCheck + .contains(space.uuid), + isSoldCheck: state.soldCheck + .contains(space.uuid), + children: _buildNestedSpaces( + context, + state, + space, + communities[index], + ), + ); }, - onExpansionChanged: () => - context.read().add( - OnSpaceExpanded( - communities[index].uuid, - space.uuid ?? '', - ), - ), - isSelected: state.selectedSpaces - .contains(space.uuid) || - state.soldCheck.contains(space.uuid), - isSoldCheck: - state.soldCheck.contains(space.uuid), - children: _buildNestedSpaces( - context, - state, - space, - communities[index], - ), - ); - }, - ).toList(), - ); - }, - ), + ).toList(), + ); + }, + ), ), - if (state.paginationIsLoading) const CircularProgressIndicator(), + if (state.paginationIsLoading) + const CircularProgressIndicator(), ], ), ); diff --git a/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart b/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart index 759cea27..a4d6628e 100644 --- a/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart +++ b/lib/pages/spaces_management/all_spaces/bloc/space_management_bloc.dart @@ -21,7 +21,8 @@ import 'package:syncrow_web/services/space_mana_api.dart'; import 'package:syncrow_web/services/space_model_mang_api.dart'; import 'package:syncrow_web/utils/constants/action_enum.dart' as custom_action; -class SpaceManagementBloc extends Bloc { +class SpaceManagementBloc + extends Bloc { final CommunitySpaceManagementApi _api; final ProductApi _productApi; final SpaceModelManagementApi _spaceModelApi; @@ -62,7 +63,8 @@ class SpaceManagementBloc extends Bloc emit) async { + void _deleteSpaceModelFromCache(DeleteSpaceModelFromCache event, + Emitter emit) async { if (_cachedSpaceModels != null) { - _cachedSpaceModels = - _cachedSpaceModels!.where((model) => model.uuid != event.deletedUuid).toList(); + _cachedSpaceModels = _cachedSpaceModels! + .where((model) => model.uuid != event.deletedUuid) + .toList(); } else { _cachedSpaceModels = await fetchSpaceModels(); } await fetchTags(); emit(SpaceModelLoaded( - communities: - state is SpaceManagementLoaded ? (state as SpaceManagementLoaded).communities : [], + communities: state is SpaceManagementLoaded + ? (state as SpaceManagementLoaded).communities + : [], products: _cachedProducts ?? [], spaceModels: List.from(_cachedSpaceModels ?? []), allTags: _cachedTags ?? [])); @@ -122,8 +127,8 @@ class SpaceManagementBloc extends Bloc.from(previousState.communities); + final updatedCommunities = + List.from(previousState.communities); for (var community in updatedCommunities) { if (community.uuid == event.communityUuid) { community.name = event.name; @@ -212,7 +219,8 @@ class SpaceManagementBloc extends Bloc> _fetchSpacesForCommunity(String communityUuid) async { + Future> _fetchSpacesForCommunity( + String communityUuid) async { final projectUuid = await ProjectManager.getProjectUUID() ?? ''; return await _api.getSpaceHierarchy(communityUuid, projectUuid); @@ -242,20 +250,23 @@ class SpaceManagementBloc extends Bloc _onBlankState(BlankStateEvent event, Emitter emit) async { + Future _onBlankState( + BlankStateEvent event, Emitter emit) async { try { final previousState = state; final projectUuid = await ProjectManager.getProjectUUID() ?? ''; var spaceBloc = event.context.read(); var spaceTreeState = event.context.read().state; - List communities = await _waitForCommunityList(spaceBloc, spaceTreeState); + List communities = + await _waitForCommunityList(spaceBloc, spaceTreeState); await fetchSpaceModels(); - await fetchTags(); + // await fetchTags(); var prevSpaceModels = await fetchSpaceModels(); - if (previousState is SpaceManagementLoaded || previousState is BlankState) { + if (previousState is SpaceManagementLoaded || + previousState is BlankState) { final prevCommunities = (previousState as dynamic).communities ?? []; emit(BlankState( communities: List.from(prevCommunities), @@ -286,7 +297,8 @@ class SpaceManagementBloc extends Bloc communities = await _waitForCommunityList(spaceBloc, spaceTreeState); + List communities = + await _waitForCommunityList(spaceBloc, spaceTreeState); // Fetch space models after communities are available final prevSpaceModels = await fetchSpaceModels(); @@ -310,8 +322,9 @@ class SpaceManagementBloc extends Bloc>(); final subscription = spaceBloc.stream.listen((state) { if (!completer.isCompleted && state.communityList.isNotEmpty) { - completer - .complete(state.searchQuery.isNotEmpty ? state.filteredCommunity : state.communityList); + completer.complete(state.searchQuery.isNotEmpty + ? state.filteredCommunity + : state.communityList); } }); try { @@ -339,7 +352,8 @@ class SpaceManagementBloc extends Bloc.from( (previousState as dynamic).communities, ); @@ -459,12 +474,15 @@ class SpaceManagementBloc extends Bloc().state; - final updatedSpaces = - await saveSpacesHierarchically(event.context, event.spaces, event.communityUuid); + final updatedSpaces = await saveSpacesHierarchically( + event.context, event.spaces, event.communityUuid); final allSpaces = await _fetchSpacesForCommunity(event.communityUuid); - emit(SpaceCreationSuccess(spaces: updatedSpaces)); - + // emit(SpaceCreationSuccess(spaces: updatedSpaces)); + // updatedSpaces.forEach( + // (element) => element.uuid, + // ); + // final lastUpdatedSpaced = updatedSpaces..addAll(allSpaces); if (previousState is SpaceManagementLoaded) { await _updateLoadedState( spaceTreeState, @@ -473,9 +491,15 @@ class SpaceManagementBloc extends Bloc space.status == SpaceStatus.deleted, + ); + event.spaces.forEach( + (space) => space.status = SpaceStatus.unchanged, + ); } } catch (e) { - emit(SpaceManagementError('Error saving spaces: $e')); + // emit(SpaceManagementError('Error saving spaces: $e')); if (previousState is SpaceManagementLoaded) { emit(previousState); @@ -515,13 +539,15 @@ class SpaceManagementBloc extends Bloc> saveSpacesHierarchically( - BuildContext context, List spaces, String communityUuid) async { + Future> saveSpacesHierarchically(BuildContext context, + List spaces, String communityUuid) async { final orderedSpaces = flattenHierarchy(spaces); final projectUuid = await ProjectManager.getProjectUUID() ?? ''; @@ -534,6 +560,14 @@ class SpaceManagementBloc extends Bloc community.uuid == communityUuid, + orElse: () => CommunityModel( + uuid: '', + createdAt: DateTime.now(), + updatedAt: DateTime.now(), + name: '', + description: '', + spaces: spaces, + ), ); } catch (e) { return []; @@ -548,9 +582,7 @@ class SpaceManagementBloc extends Bloc parentsToDelete.contains(space)); @@ -564,7 +596,7 @@ class SpaceManagementBloc extends Bloc subspaceUpdates = []; final List? prevSubspaces = prevSpace?.subspaces; @@ -575,17 +607,19 @@ class SpaceManagementBloc extends Bloc subspace.uuid == prevSubspace.uuid); + final existsInNew = newSubspaces + .any((subspace) => subspace.uuid == prevSubspace.uuid); if (!existsInNew) { subspaceUpdates.add(UpdateSubspaceTemplateModel( - action: custom_action.Action.delete, uuid: prevSubspace.uuid)); + action: custom_action.Action.delete, + uuid: prevSubspace.uuid)); } } } else if (prevSubspaces != null && newSubspaces == null) { for (var prevSubspace in prevSubspaces) { subspaceUpdates.add(UpdateSubspaceTemplateModel( - action: custom_action.Action.delete, uuid: prevSubspace.uuid)); + action: custom_action.Action.delete, + uuid: prevSubspace.uuid)); } } @@ -613,7 +647,9 @@ class SpaceManagementBloc extends Bloc tag.toCreateTagBodyModel()).toList() ?? []; + final tagBodyModels = subspace.tags + ?.map((tag) => tag.toCreateTagBodyModel()) + .toList() ?? + []; return CreateSubspaceModel() ..subspaceName = subspace.subspaceName ..tags = tagBodyModels; @@ -671,7 +710,6 @@ class SpaceManagementBloc extends Bloc emit) async { + void _onLoadSpaceModel( + SpaceModelLoadEvent event, Emitter emit) async { emit(SpaceManagementLoading()); try { @@ -757,14 +797,17 @@ class SpaceManagementBloc extends Bloc newTag.uuid == prevTag.uuid); + final existsInNew = + newTags.any((newTag) => newTag.uuid == prevTag.uuid); if (!existsInNew) { - tagUpdates.add(TagModelUpdate(action: custom_action.Action.delete, uuid: prevTag.uuid)); + tagUpdates.add(TagModelUpdate( + action: custom_action.Action.delete, uuid: prevTag.uuid)); } } } else if (prevTags != null && newTags == null) { for (var prevTag in prevTags) { - tagUpdates.add(TagModelUpdate(action: custom_action.Action.delete, uuid: prevTag.uuid)); + tagUpdates.add(TagModelUpdate( + action: custom_action.Action.delete, uuid: prevTag.uuid)); } } @@ -807,15 +850,16 @@ class SpaceManagementBloc extends Bloc findMatchingSpaces(List spaces, String targetUuid) { + List findMatchingSpaces( + List spaces, String targetUuid) { List matched = []; for (var space in spaces) { if (space.uuid == targetUuid) { matched.add(space); } - matched - .addAll(findMatchingSpaces(space.children, targetUuid)); // Recursively search in children + matched.addAll(findMatchingSpaces( + space.children, targetUuid)); // Recursively search in children } return matched; diff --git a/lib/pages/spaces_management/all_spaces/model/connection_model.dart b/lib/pages/spaces_management/all_spaces/model/connection_model.dart index a774efe2..0799d81e 100644 --- a/lib/pages/spaces_management/all_spaces/model/connection_model.dart +++ b/lib/pages/spaces_management/all_spaces/model/connection_model.dart @@ -3,23 +3,26 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model class Connection { final SpaceModel startSpace; final SpaceModel endSpace; - final String direction; - Connection({required this.startSpace, required this.endSpace, required this.direction}); + Connection({ + required this.startSpace, + required this.endSpace, + }); Map toMap() { return { - 'startUuid': startSpace.uuid ?? 'unsaved-start-space-${startSpace.name}', // Fallback for unsaved spaces - 'endUuid': endSpace.uuid ?? 'unsaved-end-space-${endSpace.name}', // Fallback for unsaved spaces - 'direction': direction, + 'startUuid': startSpace.uuid ?? + 'unsaved-start-space-${startSpace.name}', // Fallback for unsaved spaces + 'endUuid': endSpace.uuid ?? + 'unsaved-end-space-${endSpace.name}', // Fallback for unsaved spaces }; } - static Connection fromMap(Map map, Map spaces) { + static Connection fromMap( + Map map, Map spaces) { return Connection( startSpace: spaces[map['startUuid']]!, endSpace: spaces[map['endUuid']]!, - direction: map['direction'], ); } } diff --git a/lib/pages/spaces_management/all_spaces/model/product_model.dart b/lib/pages/spaces_management/all_spaces/model/product_model.dart index a4ebd550..8f905032 100644 --- a/lib/pages/spaces_management/all_spaces/model/product_model.dart +++ b/lib/pages/spaces_management/all_spaces/model/product_model.dart @@ -1,5 +1,7 @@ import 'package:syncrow_web/utils/constants/assets.dart'; +import 'selected_product_model.dart'; + class ProductModel { final String uuid; final String catName; @@ -38,6 +40,15 @@ class ProductModel { }; } + SelectedProduct toSelectedProduct(int count) { + return SelectedProduct( + productId: uuid, + count: count, + productName: name!, + product: this, + ); + } + static String _mapIconToProduct(String prodType) { const iconMapping = { '1G': Assets.Gang1SwitchIcon, diff --git a/lib/pages/spaces_management/all_spaces/model/space_model.dart b/lib/pages/spaces_management/all_spaces/model/space_model.dart index 6e744a29..f9c59d24 100644 --- a/lib/pages/spaces_management/all_spaces/model/space_model.dart +++ b/lib/pages/spaces_management/all_spaces/model/space_model.dart @@ -101,7 +101,7 @@ class SpaceModel { spaceModel: json['spaceModel'] != null ? SpaceTemplateModel.fromJson(json['spaceModel']) : null, - tags: (json['tags'] as List?) + tags: (json['productAllocations'] as List?) ?.where((item) => item is Map) // Validate type .map((item) => Tag.fromJson(item as Map)) .toList() ?? @@ -116,7 +116,6 @@ class SpaceModel { instance.incomingConnection = Connection( startSpace: instance.parent ?? instance, // Parent space endSpace: instance, // This space instance - direction: conn['direction'], ); } @@ -172,4 +171,13 @@ extension SpaceExtensions on SpaceModel { return tagValues; } + + bool isNoChangesSubmited(String name, icon, SpaceTemplateModel? spaceModel, + List? subspaces, List? tags) { + return (name == this.name && + icon == this.icon && + spaceModel == this.spaceModel && + subspaces == this.subspaces && + tags == this.tags); + } } diff --git a/lib/pages/spaces_management/all_spaces/model/subspace_model.dart b/lib/pages/spaces_management/all_spaces/model/subspace_model.dart index a89ec409..fd3e780e 100644 --- a/lib/pages/spaces_management/all_spaces/model/subspace_model.dart +++ b/lib/pages/spaces_management/all_spaces/model/subspace_model.dart @@ -27,7 +27,7 @@ class SubspaceModel { subspaceName: json['subspaceName'] ?? '', disabled: json['disabled'] ?? false, internalId: internalId, - tags: (json['tags'] as List?) + tags: (json['productAllocations'] as List?) ?.map((item) => Tag.fromJson(item)) .toList() ?? [], @@ -36,7 +36,7 @@ class SubspaceModel { Map toJson() { return { - 'uuid': uuid, + if (uuid != null) 'uuid': uuid, 'subspaceName': subspaceName, 'disabled': disabled, 'tags': tags?.map((e) => e.toJson()).toList() ?? [], diff --git a/lib/pages/spaces_management/all_spaces/model/tag.dart b/lib/pages/spaces_management/all_spaces/model/tag.dart index 8959986c..a7ec1e15 100644 --- a/lib/pages/spaces_management/all_spaces/model/tag.dart +++ b/lib/pages/spaces_management/all_spaces/model/tag.dart @@ -23,10 +23,13 @@ class Tag extends BaseTag { final String internalId = json['internalId'] ?? const Uuid().v4(); return Tag( - uuid: json['uuid'] ?? '', + //TODO:insure UUId for tag or prodAlloc + uuid: json['name'] != null ? json['uuid'] : json['tag']?['uuid'] ?? '', internalId: internalId, - tag: json['name'] ?? '', - product: json['product'] != null ? ProductModel.fromMap(json['product']) : null, + tag: json['name'] ?? json['tag']?['name'] ?? '', + product: json['product'] != null + ? ProductModel.fromMap(json['product']) + : null, ); } @@ -49,9 +52,10 @@ class Tag extends BaseTag { Map toJson() { return { - 'uuid': uuid, - 'tag': tag, - 'product': product?.toMap(), + if (uuid != null) 'uuid': uuid, + 'name': tag, + 'productUuid': product?.uuid, + // .toMap(), }; } } diff --git a/lib/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart index a93679d0..4f68fb7e 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/community_structure_widget.dart @@ -67,7 +67,8 @@ class _CommunityStructureAreaState extends State { void initState() { super.initState(); spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : []; - connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : []; + connections = + widget.spaces.isNotEmpty ? createConnections(widget.spaces) : []; _adjustCanvasSizeForSpaces(); _nameController = TextEditingController( text: widget.selectedCommunity?.name ?? '', @@ -96,13 +97,15 @@ class _CommunityStructureAreaState extends State { if (oldWidget.spaces != widget.spaces) { setState(() { spaces = widget.spaces.isNotEmpty ? flattenSpaces(widget.spaces) : []; - connections = widget.spaces.isNotEmpty ? createConnections(widget.spaces) : []; + connections = + widget.spaces.isNotEmpty ? createConnections(widget.spaces) : []; _adjustCanvasSizeForSpaces(); realignTree(); }); } - if (widget.selectedSpace != oldWidget.selectedSpace && widget.selectedSpace != null) { + if (widget.selectedSpace != oldWidget.selectedSpace && + widget.selectedSpace != null) { WidgetsBinding.instance.addPostFrameCallback((_) { _moveToSpace(widget.selectedSpace!); }); @@ -185,7 +188,8 @@ class _CommunityStructureAreaState extends State { connection, widget.selectedSpace) ? 1.0 : 0.3, // Adjust opacity - child: CustomPaint(painter: CurvedLinePainter([connection])), + child: CustomPaint( + painter: CurvedLinePainter([connection])), ), for (var entry in spaces.asMap().entries) if (entry.value.status != SpaceStatus.deleted && @@ -195,11 +199,11 @@ class _CommunityStructureAreaState extends State { top: entry.value.position.dy, child: SpaceCardWidget( index: entry.key, - onButtonTap: (int index, Offset newPosition, String direction) { + onButtonTap: (int index, Offset newPosition) { _showCreateSpaceDialog(screenSize, - position: spaces[index].position + newPosition, + position: + spaces[index].position + newPosition, parentIndex: index, - direction: direction, projectTags: widget.projectTags); }, position: entry.value.position, @@ -210,8 +214,9 @@ class _CommunityStructureAreaState extends State { _updateNodePosition(entry.value, newPosition); }, buildSpaceContainer: (int index) { - final bool isHighlighted = SpaceHelper.isHighlightedSpace( - spaces[index], widget.selectedSpace); + final bool isHighlighted = + SpaceHelper.isHighlightedSpace( + spaces[index], widget.selectedSpace); return Opacity( opacity: isHighlighted ? 1.0 : 0.3, @@ -289,7 +294,6 @@ class _CommunityStructureAreaState extends State { void _showCreateSpaceDialog(Size screenSize, {Offset? position, int? parentIndex, - String? direction, double? canvasWidth, double? canvasHeight, required List projectTags}) { @@ -299,19 +303,25 @@ class _CommunityStructureAreaState extends State { return CreateSpaceDialog( products: widget.products, spaceModels: widget.spaceModels, - allTags: TagHelper.getAllTagValues(widget.communities, widget.spaceModels), + allTags: + TagHelper.getAllTagValues(widget.communities, widget.spaceModels), parentSpace: parentIndex != null ? spaces[parentIndex] : null, projectTags: projectTags, - onCreateSpace: (String name, String icon, List selectedProducts, - SpaceTemplateModel? spaceModel, List? subspaces, List? tags) { + onCreateSpace: (String name, + String icon, + List selectedProducts, + SpaceTemplateModel? spaceModel, + List? subspaces, + List? tags) { setState(() { // Set the first space in the center or use passed position Offset newPosition; if (parentIndex != null) { - newPosition = - getBalancedChildPosition(spaces[parentIndex]); // Ensure balanced position + newPosition = getBalancedChildPosition( + spaces[parentIndex]); // Ensure balanced position } else { - newPosition = position ?? ConnectionHelper.getCenterPosition(screenSize); + newPosition = + position ?? ConnectionHelper.getCenterPosition(screenSize); } SpaceModel newSpace = SpaceModel( @@ -325,14 +335,13 @@ class _CommunityStructureAreaState extends State { subspaces: subspaces, tags: tags); - if (parentIndex != null && direction != null) { + if (parentIndex != null) { SpaceModel parentSpace = spaces[parentIndex]; parentSpace.internalId = spaces[parentIndex].internalId; newSpace.parent = parentSpace; final newConnection = Connection( startSpace: parentSpace, endSpace: newSpace, - direction: direction, ); connections.add(newConnection); newSpace.incomingConnection = newConnection; @@ -360,16 +369,30 @@ class _CommunityStructureAreaState extends State { name: widget.selectedSpace!.name, icon: widget.selectedSpace!.icon, projectTags: widget.projectTags, - parentSpace: - SpaceHelper.findSpaceByInternalId(widget.selectedSpace?.parent?.internalId, spaces), + parentSpace: SpaceHelper.findSpaceByInternalId( + widget.selectedSpace?.parent?.internalId, spaces), editSpace: widget.selectedSpace, currentSpaceModel: widget.selectedSpace?.spaceModel, tags: widget.selectedSpace?.tags, subspaces: widget.selectedSpace?.subspaces, isEdit: true, - allTags: TagHelper.getAllTagValues(widget.communities, widget.spaceModels), - onCreateSpace: (String name, String icon, List selectedProducts, - SpaceTemplateModel? spaceModel, List? subspaces, List? tags) { + allTags: TagHelper.getAllTagValues( + widget.communities, widget.spaceModels), + onCreateSpace: (String name, + String icon, + List selectedProducts, + SpaceTemplateModel? spaceModel, + List? subspaces, + List? tags) { + if (widget.selectedSpace!.isNoChangesSubmited( + name, + icon, + spaceModel, + subspaces, + tags, + )) { + return; + } setState(() { // Update the space's properties widget.selectedSpace!.name = name; @@ -379,7 +402,8 @@ class _CommunityStructureAreaState extends State { widget.selectedSpace!.tags = tags; if (widget.selectedSpace!.status != SpaceStatus.newSpace) { - widget.selectedSpace!.status = SpaceStatus.modified; // Mark as modified + widget.selectedSpace!.status = + SpaceStatus.modified; // Mark as modified } for (var space in spaces) { @@ -410,7 +434,8 @@ class _CommunityStructureAreaState extends State { Map idToSpace = {}; void flatten(SpaceModel space) { - if (space.status == SpaceStatus.deleted || space.status == SpaceStatus.parentDeleted) { + if (space.status == SpaceStatus.deleted || + space.status == SpaceStatus.parentDeleted) { return; } result.add(space); @@ -447,7 +472,6 @@ class _CommunityStructureAreaState extends State { Connection( startSpace: parent, endSpace: child, - direction: "down", ), ); @@ -532,13 +556,16 @@ class _CommunityStructureAreaState extends State { void _selectSpace(BuildContext context, SpaceModel space) { context.read().add( - SelectSpaceEvent(selectedCommunity: widget.selectedCommunity, selectedSpace: space), + SelectSpaceEvent( + selectedCommunity: widget.selectedCommunity, + selectedSpace: space), ); } void _deselectSpace(BuildContext context) { context.read().add( - SelectSpaceEvent(selectedCommunity: widget.selectedCommunity, selectedSpace: null), + SelectSpaceEvent( + selectedCommunity: widget.selectedCommunity, selectedSpace: null), ); } @@ -708,7 +735,8 @@ class _CommunityStructureAreaState extends State { SpaceModel duplicated = _deepCloneSpaceTree(space, parent: parent); - duplicated.position = Offset(space.position.dx + 300, space.position.dy + 100); + duplicated.position = + Offset(space.position.dx + 300, space.position.dy + 100); List duplicatedSubtree = []; void collectSubtree(SpaceModel node) { duplicatedSubtree.add(node); @@ -726,7 +754,6 @@ class _CommunityStructureAreaState extends State { final newConnection = Connection( startSpace: parent, endSpace: duplicated, - direction: "down", ); connections.add(newConnection); duplicated.incomingConnection = newConnection; @@ -739,7 +766,8 @@ class _CommunityStructureAreaState extends State { } SpaceModel _deepCloneSpaceTree(SpaceModel original, {SpaceModel? parent}) { - final duplicatedName = SpaceHelper.generateUniqueSpaceName(original.name, spaces); + final duplicatedName = + SpaceHelper.generateUniqueSpaceName(original.name, spaces); final newSpace = SpaceModel( name: duplicatedName, @@ -761,7 +789,6 @@ class _CommunityStructureAreaState extends State { final newConnection = Connection( startSpace: newSpace, endSpace: duplicatedChild, - direction: "down", ); connections.add(newConnection); diff --git a/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/devices_part_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/devices_part_widget.dart new file mode 100644 index 00000000..94896554 --- /dev/null +++ b/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/devices_part_widget.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../../../../common/edit_chip.dart'; +import '../../../../../utils/color_manager.dart'; +import '../../../helper/tag_helper.dart'; +import '../../../space_model/widgets/button_content_widget.dart'; +import '../../model/subspace_model.dart'; +import '../../model/tag.dart'; + +class DevicesPartWidget extends StatelessWidget { + const DevicesPartWidget({ + super.key, + required this.tags, + required this.subspaces, + required this.screenWidth, + required this.onEditChip, + required this.onTextButtonPressed, + required this.isTagsAndSubspaceModelDisabled, + }); + final bool isTagsAndSubspaceModelDisabled; + final void Function() onEditChip; + final void Function() onTextButtonPressed; + final double screenWidth; + final List? tags; + final List? subspaces; + @override + Widget build(BuildContext context) { + return Column( + children: [ + (tags?.isNotEmpty == true || + subspaces?.any( + (subspace) => subspace.tags?.isNotEmpty == true) == + true) + ? SizedBox( + width: screenWidth * 0.25, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, // Border width + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + // Combine tags from spaceModel and subspaces + ...TagHelper.groupTags([ + ...?tags, + ...?subspaces?.expand((subspace) => subspace.tags ?? []) + ]).entries.map( + (entry) => Chip( + avatar: SizedBox( + width: 24, + height: 24, + child: SvgPicture.asset( + entry.key.icon ?? 'assets/icons/gateway.svg', + fit: BoxFit.contain, + ), + ), + label: Text( + 'x${entry.value}', // Show count + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.spaceColor), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + side: const BorderSide( + color: ColorsManager.spaceColor, + ), + ), + ), + ), + + EditChip(onTap: onEditChip) + ], + ), + ), + ) + : TextButton( + onPressed: onTextButtonPressed, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + ), + child: ButtonContentWidget( + icon: Icons.add, + label: 'Add Devices', + disabled: isTagsAndSubspaceModelDisabled, + ), + ) + ], + ); + } +} diff --git a/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/icon_choose_part_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/icon_choose_part_widget.dart new file mode 100644 index 00000000..59a100a3 --- /dev/null +++ b/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/icon_choose_part_widget.dart @@ -0,0 +1,65 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import '../../../../../utils/color_manager.dart'; +import '../../../../../utils/constants/assets.dart'; + +class IconChoosePartWidget extends StatelessWidget { + const IconChoosePartWidget({ + super.key, + required this.selectedIcon, + required this.showIconSelection, + required this.screenWidth, + }); + final double screenWidth; + final String selectedIcon; + final void Function() showIconSelection; + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + // crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const SizedBox(height: 50), + Stack( + alignment: Alignment.center, + children: [ + Container( + width: screenWidth * 0.1, + height: screenWidth * 0.1, + decoration: const BoxDecoration( + color: ColorsManager.boxColor, + shape: BoxShape.circle, + ), + ), + SvgPicture.asset( + selectedIcon, + width: screenWidth * 0.04, + height: screenWidth * 0.04, + ), + Positioned( + top: 20, + right: 20, + child: InkWell( + onTap: showIconSelection, + child: Container( + width: 24, + height: 24, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: SvgPicture.asset( + Assets.iconEdit, + width: 16, + height: 16, + ), + ), + ), + ), + ], + ), + ], + ); + } +} diff --git a/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/space_model_linking_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/space_model_linking_widget.dart new file mode 100644 index 00000000..cd9ae470 --- /dev/null +++ b/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/space_model_linking_widget.dart @@ -0,0 +1,89 @@ +import 'package:flutter/material.dart'; + +import '../../../../../utils/color_manager.dart'; +import '../../../../../utils/constants/assets.dart'; +import '../../../space_model/models/space_template_model.dart'; +import '../../../space_model/widgets/button_content_widget.dart'; + +class SpaceModelLinkingWidget extends StatelessWidget { + const SpaceModelLinkingWidget({ + super.key, + required this.onDeleted, + required this.onPressed, + required this.screenWidth, + required this.selectedSpaceModel, + required this.isSpaceModelDisabled, + }); + final bool isSpaceModelDisabled; + final void Function()? onDeleted; + final void Function()? onPressed; + final double screenWidth; + final SpaceTemplateModel? selectedSpaceModel; + @override + Widget build(BuildContext context) { + return Column( + children: [ + selectedSpaceModel == null + ? TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + ), + onPressed: onPressed, + child: ButtonContentWidget( + svgAssets: Assets.link, + label: 'Link a space model', + disabled: isSpaceModelDisabled, + ), + ) + : Container( + width: screenWidth * 0.25, + padding: const EdgeInsets.symmetric( + vertical: 10.0, horizontal: 16.0), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(10), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + Chip( + label: Text( + selectedSpaceModel?.modelName ?? '', + style: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.spaceColor), + ), + backgroundColor: ColorsManager.whiteColors, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(8), + side: const BorderSide( + color: ColorsManager.transparentColor, + width: 0, + ), + ), + deleteIcon: Container( + width: 24, + height: 24, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.5, + ), + ), + child: const Icon( + Icons.close, + size: 16, + color: ColorsManager.lightGrayColor, + ), + ), + onDeleted: onDeleted), + ], + ), + ), + ], + ); + } +} diff --git a/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/space_name_textfield_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/space_name_textfield_widget.dart new file mode 100644 index 00000000..600cb8ad --- /dev/null +++ b/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/space_name_textfield_widget.dart @@ -0,0 +1,82 @@ +import 'package:flutter/material.dart'; + +import '../../../../../utils/color_manager.dart'; + +class SpaceNameTextfieldWidget extends StatelessWidget { + SpaceNameTextfieldWidget({ + super.key, + required this.isNameFieldExist, + required this.isNameFieldInvalid, + required this.onChange, + required this.screenWidth, + required this.nameController, + }); + TextEditingController nameController; + final void Function(String value) onChange; + final double screenWidth; + bool isNameFieldExist; + bool isNameFieldInvalid; + + @override + Widget build(BuildContext context) { + return Column( + children: [ + SizedBox( + width: screenWidth * 0.25, + child: TextField( + controller: nameController, + onChanged: onChange, + style: Theme.of(context).textTheme.bodyMedium, + decoration: InputDecoration( + hintText: 'Please enter the name', + hintStyle: Theme.of(context) + .textTheme + .bodyMedium! + .copyWith(color: ColorsManager.lightGrayColor), + filled: true, + fillColor: ColorsManager.boxColor, + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: BorderSide( + color: isNameFieldInvalid || isNameFieldExist + ? ColorsManager.red + : ColorsManager.boxColor, + width: 1.5, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide( + color: ColorsManager.boxColor, + width: 1.5, + ), + ), + ), + ), + ), + if (isNameFieldInvalid) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Space name should not be empty.', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + if (isNameFieldExist) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + '*Name already exist', + style: Theme.of(context) + .textTheme + .bodySmall + ?.copyWith(color: ColorsManager.red), + ), + ), + ], + ); + } +} diff --git a/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/sub_space_part_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/sub_space_part_widget.dart new file mode 100644 index 00000000..e518877a --- /dev/null +++ b/lib/pages/spaces_management/all_spaces/widgets/create_space_widgets/sub_space_part_widget.dart @@ -0,0 +1,101 @@ +import 'package:flutter/material.dart'; + +import '../../../../../common/edit_chip.dart'; +import '../../../../../utils/color_manager.dart'; +import '../../../space_model/widgets/button_content_widget.dart'; +import '../../../space_model/widgets/subspace_name_label_widget.dart'; +import '../../model/subspace_model.dart'; + +class SubSpacePartWidget extends StatefulWidget { + SubSpacePartWidget({ + super.key, + required this.subspaces, + required this.onPressed, + required this.isTagsAndSubspaceModelDisabled, + required this.screenWidth, + required this.editChipOnTap, + }); + double screenWidth; + bool isTagsAndSubspaceModelDisabled; + final void Function() editChipOnTap; + List? subspaces; + final void Function() onPressed; + + @override + State createState() => _SubSpacePartWidgetState(); +} + +class _SubSpacePartWidgetState extends State { + @override + Widget build(BuildContext context) { + return Column( + children: [ + widget.subspaces == null || widget.subspaces!.isEmpty + ? TextButton( + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + overlayColor: ColorsManager.transparentColor, + ), + onPressed: widget.onPressed, + child: ButtonContentWidget( + icon: Icons.add, + label: 'Create Sub Spaces', + disabled: widget.isTagsAndSubspaceModelDisabled, + ), + ) + : SizedBox( + width: widget.screenWidth * 0.25, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + color: ColorsManager.textFieldGreyColor, + borderRadius: BorderRadius.circular(15), + border: Border.all( + color: ColorsManager.textFieldGreyColor, + width: 3.0, // Border width + ), + ), + child: Wrap( + spacing: 8.0, + runSpacing: 8.0, + children: [ + if (widget.subspaces != null) + ...widget.subspaces!.map((subspace) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + SubspaceNameDisplayWidget( + text: subspace.subspaceName, + validateName: (updatedName) { + bool nameExists = widget.subspaces!.any((s) { + bool isSameId = + s.internalId == subspace.internalId; + bool isSameName = + s.subspaceName.trim().toLowerCase() == + updatedName.trim().toLowerCase(); + + return !isSameId && isSameName; + }); + + return !nameExists; + }, + onNameChanged: (updatedName) { + setState(() { + subspace.subspaceName = updatedName; + }); + }, + ), + ], + ); + }), + EditChip( + onTap: widget.editChipOnTap, + ) + ], + ), + ), + ) + ], + ); + } +} diff --git a/lib/pages/spaces_management/all_spaces/widgets/curved_line_painter.dart b/lib/pages/spaces_management/all_spaces/widgets/curved_line_painter.dart index 2b85acfd..d8291110 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/curved_line_painter.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/curved_line_painter.dart @@ -30,28 +30,13 @@ class CurvedLinePainter extends CustomPainter { Offset end = connection.endSpace.position + const Offset(75, 0); // Center top of end space - if (connection.direction == 'down') { - // Curved line for down connections - final controlPoint = Offset((start.dx + end.dx) / 2, start.dy + 50); - final path = Path() - ..moveTo(start.dx, start.dy) - ..quadraticBezierTo(controlPoint.dx, controlPoint.dy, end.dx, end.dy); - canvas.drawPath(path, paint); - } else if (connection.direction == 'right') { - start = connection.startSpace.position + - const Offset(150, 30); // Right center - end = connection.endSpace.position + const Offset(0, 30); // Left center + // Curved line for down connections + final controlPoint = Offset((start.dx + end.dx) / 2, start.dy + 50); + final path = Path() + ..moveTo(start.dx, start.dy) + ..quadraticBezierTo(controlPoint.dx, controlPoint.dy, end.dx, end.dy); + canvas.drawPath(path, paint); - canvas.drawLine(start, end, paint); - } else if (connection.direction == 'left') { - start = - connection.startSpace.position + const Offset(0, 30); // Left center - end = connection.endSpace.position + - const Offset(150, 30); // Right center - - canvas.drawLine(start, end, paint); - } - final dotPaint = Paint()..color = ColorsManager.blackColor; canvas.drawCircle(start, 5, dotPaint); // Start dot canvas.drawCircle(end, 5, dotPaint); // End dot diff --git a/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart b/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart index e9dde6f8..8cf30f7c 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/dialogs/create_space_dialog.dart @@ -1,6 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; -import 'package:syncrow_web/common/edit_chip.dart'; import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/spaces_management/add_device_type/views/add_device_type_widget.dart'; @@ -9,6 +7,11 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_pr import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/devices_part_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/icon_choose_part_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/space_model_linking_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/space_name_textfield_widget.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/create_space_widgets/sub_space_part_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/widgets/dialogs/icon_selection_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart'; @@ -16,8 +19,6 @@ import 'package:syncrow_web/pages/spaces_management/helper/space_helper.dart'; import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart'; import 'package:syncrow_web/pages/spaces_management/link_space_model/view/link_space_model_dialog.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; -import 'package:syncrow_web/pages/spaces_management/space_model/widgets/button_content_widget.dart'; -import 'package:syncrow_web/pages/spaces_management/space_model/widgets/subspace_name_label_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/constants/space_icon_const.dart'; @@ -82,8 +83,10 @@ class CreateSpaceDialogState extends State { super.initState(); selectedIcon = widget.icon ?? Assets.location; nameController = TextEditingController(text: widget.name ?? ''); - selectedProducts = widget.selectedProducts.isNotEmpty ? widget.selectedProducts : []; - isOkButtonEnabled = enteredName.isNotEmpty || nameController.text.isNotEmpty; + selectedProducts = + widget.selectedProducts.isNotEmpty ? widget.selectedProducts : []; + isOkButtonEnabled = + enteredName.isNotEmpty || nameController.text.isNotEmpty; if (widget.currentSpaceModel != null) { subspaces = []; tags = []; @@ -96,13 +99,15 @@ class CreateSpaceDialogState extends State { @override Widget build(BuildContext context) { - bool isSpaceModelDisabled = - (tags != null && tags!.isNotEmpty || subspaces != null && subspaces!.isNotEmpty); + bool isSpaceModelDisabled = (tags != null && tags!.isNotEmpty || + subspaces != null && subspaces!.isNotEmpty); bool isTagsAndSubspaceModelDisabled = (selectedSpaceModel != null); final screenWidth = MediaQuery.of(context).size.width; return AlertDialog( - title: widget.isEdit ? const Text('Edit Space') : const Text('Create New Space'), + title: widget.isEdit + ? const Text('Edit Space') + : const Text('Create New Space'), backgroundColor: ColorsManager.whiteColors, content: SizedBox( width: screenWidth * 0.5, @@ -112,50 +117,10 @@ class CreateSpaceDialogState extends State { children: [ Expanded( flex: 1, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - // crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const SizedBox(height: 50), - Stack( - alignment: Alignment.center, - children: [ - Container( - width: screenWidth * 0.1, - height: screenWidth * 0.1, - decoration: const BoxDecoration( - color: ColorsManager.boxColor, - shape: BoxShape.circle, - ), - ), - SvgPicture.asset( - selectedIcon, - width: screenWidth * 0.04, - height: screenWidth * 0.04, - ), - Positioned( - top: 20, - right: 20, - child: InkWell( - onTap: _showIconSelectionDialog, - child: Container( - width: 24, - height: 24, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: SvgPicture.asset( - Assets.iconEdit, - width: 16, - height: 16, - ), - ), - ), - ), - ], - ), - ], + child: IconChoosePartWidget( + selectedIcon: selectedIcon, + showIconSelection: _showIconSelectionDialog, + screenWidth: screenWidth, ), ), const SizedBox(width: 20), @@ -164,342 +129,146 @@ class CreateSpaceDialogState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SizedBox( - width: screenWidth * 0.25, - child: TextField( - controller: nameController, - onChanged: (value) { - enteredName = value.trim(); - setState(() { - isNameFieldExist = false; - isOkButtonEnabled = false; - isNameFieldInvalid = value.isEmpty; + SpaceNameTextfieldWidget( + isNameFieldExist: isNameFieldExist, + isNameFieldInvalid: isNameFieldInvalid, + nameController: nameController, + screenWidth: screenWidth, + onChange: (value) { + enteredName = value.trim(); + setState(() { + isNameFieldExist = false; + isOkButtonEnabled = false; + isNameFieldInvalid = value.isEmpty; - if (!isNameFieldInvalid) { - if (SpaceHelper.isNameConflict( - value, widget.parentSpace, widget.editSpace)) { - isNameFieldExist = true; - isOkButtonEnabled = false; - } else { - isNameFieldExist = false; - isOkButtonEnabled = true; - } + if (!isNameFieldInvalid) { + if (SpaceHelper.isNameConflict( + value, widget.parentSpace, widget.editSpace)) { + isNameFieldExist = true; + isOkButtonEnabled = false; + } else { + isNameFieldExist = false; + isOkButtonEnabled = true; } - }); - }, - style: Theme.of(context).textTheme.bodyMedium, - decoration: InputDecoration( - hintText: 'Please enter the name', - hintStyle: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.lightGrayColor), - filled: true, - fillColor: ColorsManager.boxColor, - enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: BorderSide( - color: isNameFieldInvalid || isNameFieldExist - ? ColorsManager.red - : ColorsManager.boxColor, - width: 1.5, - ), - ), - focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(10), - borderSide: const BorderSide( - color: ColorsManager.boxColor, - width: 1.5, - ), - ), - ), - ), + } + }); + }, ), - if (isNameFieldInvalid) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - '*Space name should not be empty.', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: ColorsManager.red), - ), - ), - if (isNameFieldExist) - Padding( - padding: const EdgeInsets.only(top: 8.0), - child: Text( - '*Name already exist', - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: ColorsManager.red), - ), - ), const SizedBox(height: 10), - selectedSpaceModel == null - ? TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - ), - onPressed: () { - isSpaceModelDisabled ? null : _showLinkSpaceModelDialog(context); - }, - child: ButtonContentWidget( - svgAssets: Assets.link, - label: 'Link a space model', - disabled: isSpaceModelDisabled, - ), - ) - : Container( - width: screenWidth * 0.25, - padding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 16.0), - decoration: BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.circular(10), - ), - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: [ - Chip( - label: Text( - selectedSpaceModel?.modelName ?? '', - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: ColorsManager.spaceColor), - ), - backgroundColor: ColorsManager.whiteColors, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(8), - side: const BorderSide( - color: ColorsManager.transparentColor, - width: 0, - ), - ), - deleteIcon: Container( - width: 24, - height: 24, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: ColorsManager.lightGrayColor, - width: 1.5, - ), - ), - child: const Icon( - Icons.close, - size: 16, - color: ColorsManager.lightGrayColor, - ), - ), - onDeleted: () => setState(() { - this.selectedSpaceModel = null; - subspaces = widget.subspaces ?? []; - tags = widget.tags ?? []; - })), - ], - ), - ), + // SpaceModelLinkingWidget( + // isSpaceModelDisabled: true, + // // isSpaceModelDisabled, + // onPressed: () { + // isSpaceModelDisabled + // ? null + // : _showLinkSpaceModelDialog(context); + // }, + // onDeleted: () => setState(() { + // selectedSpaceModel = null; + // subspaces = widget.subspaces ?? []; + // tags = widget.tags ?? []; + // }), + // screenWidth: screenWidth, + // selectedSpaceModel: selectedSpaceModel, + // ), const SizedBox(height: 25), - Row( - children: [ - const Expanded( - child: Divider( - color: ColorsManager.neutralGray, - thickness: 1.0, - ), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 6.0), - child: Text( - 'OR', - style: Theme.of(context) - .textTheme - .bodyMedium - ?.copyWith(fontWeight: FontWeight.bold), - ), - ), - const Expanded( - child: Divider( - color: ColorsManager.neutralGray, - thickness: 1.0, - ), - ), - ], + // Row( + // children: [ + // const Expanded( + // child: Divider( + // color: ColorsManager.neutralGray, + // thickness: 1.0, + // ), + // ), + // Padding( + // padding: const EdgeInsets.symmetric(horizontal: 6.0), + // child: Text( + // 'OR', + // style: Theme.of(context) + // .textTheme + // .bodyMedium + // ?.copyWith(fontWeight: FontWeight.bold), + // ), + // ), + // const Expanded( + // child: Divider( + // color: ColorsManager.neutralGray, + // thickness: 1.0, + // ), + // ), + // ], + // ), + const SizedBox(height: 25), + SubSpacePartWidget( + subspaces: subspaces, + onPressed: () { + isTagsAndSubspaceModelDisabled + ? null + : _showSubSpaceDialog( + context, + enteredName, + [], + false, + widget.products, + subspaces, + ); + }, + isTagsAndSubspaceModelDisabled: + isTagsAndSubspaceModelDisabled, + screenWidth: screenWidth, + editChipOnTap: () async { + _showSubSpaceDialog( + context, + enteredName, + [], + true, + widget.products, + subspaces, + ); + }, ), - const SizedBox(height: 25), - subspaces == null || subspaces!.isEmpty - ? TextButton( - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - overlayColor: ColorsManager.transparentColor, - ), - onPressed: () async { - isTagsAndSubspaceModelDisabled - ? null - : _showSubSpaceDialog( - context, enteredName, [], false, widget.products, subspaces); - }, - child: ButtonContentWidget( - icon: Icons.add, - label: 'Create Sub Space', - disabled: isTagsAndSubspaceModelDisabled, - ), - ) - : SizedBox( - width: screenWidth * 0.25, - child: Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: ColorsManager.textFieldGreyColor, - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: ColorsManager.textFieldGreyColor, - width: 3.0, // Border width - ), - ), - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: [ - if (subspaces != null) - ...subspaces!.map((subspace) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SubspaceNameDisplayWidget( - text: subspace.subspaceName, - validateName: (updatedName) { - bool nameExists = subspaces!.any((s) { - bool isSameId = s.internalId == subspace.internalId; - bool isSameName = - s.subspaceName.trim().toLowerCase() == - updatedName.trim().toLowerCase(); - - return !isSameId && isSameName; - }); - - return !nameExists; - }, - onNameChanged: (updatedName) { - setState(() { - subspace.subspaceName = updatedName; - }); - }, - ), - ], - ); - }), - EditChip( - onTap: () async { - _showSubSpaceDialog(context, enteredName, [], true, - widget.products, subspaces); - }, - ) - ], - ), - ), - ), const SizedBox(height: 10), - (tags?.isNotEmpty == true || - subspaces?.any((subspace) => subspace.tags?.isNotEmpty == true) == true) - ? SizedBox( - width: screenWidth * 0.25, - child: Container( - padding: const EdgeInsets.all(8.0), - decoration: BoxDecoration( - color: ColorsManager.textFieldGreyColor, - borderRadius: BorderRadius.circular(15), - border: Border.all( - color: ColorsManager.textFieldGreyColor, - width: 3.0, // Border width - ), - ), - child: Wrap( - spacing: 8.0, - runSpacing: 8.0, - children: [ - // Combine tags from spaceModel and subspaces - ...TagHelper.groupTags([ - ...?tags, - ...?subspaces?.expand((subspace) => subspace.tags ?? []) - ]).entries.map( - (entry) => Chip( - avatar: SizedBox( - width: 24, - height: 24, - child: SvgPicture.asset( - entry.key.icon ?? 'assets/icons/gateway.svg', - fit: BoxFit.contain, - ), - ), - label: Text( - 'x${entry.value}', // Show count - style: Theme.of(context) - .textTheme - .bodySmall - ?.copyWith(color: ColorsManager.spaceColor), - ), - backgroundColor: ColorsManager.whiteColors, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(16), - side: const BorderSide( - color: ColorsManager.spaceColor, - ), - ), - ), - ), - - EditChip(onTap: () async { - await showDialog( - context: context, - builder: (context) => AssignTagDialog( - products: widget.products, - subspaces: subspaces, - allTags: widget.allTags, - addedProducts: - TagHelper.createInitialSelectedProductsForTags( - tags ?? [], subspaces), - title: 'Edit Device', - initialTags: TagHelper.generateInitialForTags( - spaceTags: tags, subspaces: subspaces), - spaceName: widget.name ?? '', - projectTags: widget.projectTags, - onSave: (updatedTags, updatedSubspaces) { - setState(() { - tags = updatedTags; - subspaces = updatedSubspaces; - }); - }, - ), - ); - }) - ], - ), - ), - ) - : TextButton( - onPressed: () { - isTagsAndSubspaceModelDisabled - ? null - : _showTagCreateDialog( - context, - enteredName, - widget.isEdit, - widget.products, - ); + DevicesPartWidget( + tags: tags, + subspaces: subspaces, + screenWidth: screenWidth, + isTagsAndSubspaceModelDisabled: + isTagsAndSubspaceModelDisabled, + onEditChip: () async { + await showDialog( + context: context, + builder: (context) => AssignTagDialog( + products: widget.products, + subspaces: subspaces, + allTags: widget.allTags, + addedProducts: + TagHelper.createInitialSelectedProductsForTags( + tags ?? [], subspaces), + title: 'Edit Device', + initialTags: TagHelper.generateInitialForTags( + spaceTags: tags, subspaces: subspaces), + spaceName: widget.name ?? '', + projectTags: widget.projectTags, + onSave: (updatedTags, updatedSubspaces) { + setState(() { + tags = updatedTags; + subspaces = updatedSubspaces; + }); }, - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - ), - child: ButtonContentWidget( - icon: Icons.add, - label: 'Add Devices', - disabled: isTagsAndSubspaceModelDisabled, - )) + ), + ); + }, + onTextButtonPressed: () { + isTagsAndSubspaceModelDisabled + ? null + : _showTagCreateDialog( + context, + enteredName, + widget.isEdit, + widget.products, + ); + }, + ) ], ), ), @@ -529,17 +298,25 @@ class CreateSpaceDialogState extends State { } else if (isNameFieldExist) { return; } else { - String newName = enteredName.isNotEmpty ? enteredName : (widget.name ?? ''); + String newName = enteredName.isNotEmpty + ? enteredName + : (widget.name ?? ''); if (newName.isNotEmpty) { - widget.onCreateSpace(newName, selectedIcon, selectedProducts, - selectedSpaceModel, subspaces, tags); + widget.onCreateSpace( + newName, + selectedIcon, + selectedProducts, + selectedSpaceModel, + subspaces, + tags); Navigator.of(context).pop(); } } }, borderRadius: 10, - backgroundColor: - isOkButtonEnabled ? ColorsManager.secondaryColor : ColorsManager.grayColor, + backgroundColor: isOkButtonEnabled + ? ColorsManager.secondaryColor + : ColorsManager.grayColor, foregroundColor: ColorsManager.whiteColors, child: const Text('OK'), ), @@ -550,6 +327,7 @@ class CreateSpaceDialogState extends State { ); } +//dialooogggs void _showIconSelectionDialog() { showDialog( context: context, @@ -586,26 +364,50 @@ class CreateSpaceDialogState extends State { ); } - void _showSubSpaceDialog(BuildContext context, String name, final List? spaceTags, - bool isEdit, List? products, final List? existingSubSpaces) { + void _showSubSpaceDialog( + BuildContext context, + String name, + final List? spaceTags, + bool isEdit, + List? products, + final List? existingSubSpaces, + ) { showDialog( context: context, builder: (BuildContext context) { return CreateSubSpaceDialog( spaceName: name, - dialogTitle: isEdit ? 'Edit Sub-space' : 'Create Sub-space', + dialogTitle: isEdit ? 'Edit Sub-spaces' : 'Create Sub-spaces', products: products, existingSubSpaces: existingSubSpaces, - onSave: (slectedSubspaces) { + onSave: (slectedSubspaces, updatedSubSpaces) { final List tagsToAppendToSpace = []; - if (slectedSubspaces != null) { - final updatedIds = slectedSubspaces.map((s) => s.internalId).toSet(); + if (slectedSubspaces != null && slectedSubspaces.isNotEmpty) { + final updatedIds = + slectedSubspaces.map((s) => s.internalId).toSet(); if (existingSubSpaces != null) { - final deletedSubspaces = - existingSubSpaces.where((s) => !updatedIds.contains(s.internalId)).toList(); + final deletedSubspaces = existingSubSpaces + .where((s) => !updatedIds.contains(s.internalId)) + .toList(); for (var s in deletedSubspaces) { if (s.tags != null) { + s.tags!.forEach( + (tag) => tag.location = null, + ); + tagsToAppendToSpace.addAll(s.tags!); + } + } + } + } else { + if (existingSubSpaces != null) { + final deletedSubspaces = existingSubSpaces; + + for (var s in deletedSubspaces) { + if (s.tags != null) { + s.tags!.forEach( + (tag) => tag.location = null, + ); tagsToAppendToSpace.addAll(s.tags!); } } @@ -623,15 +425,16 @@ class CreateSpaceDialogState extends State { ); } - void _showTagCreateDialog( - BuildContext context, String name, bool isEdit, List? products) { + void _showTagCreateDialog(BuildContext context, String name, bool isEdit, + List? products) { isEdit ? showDialog( context: context, builder: (BuildContext context) { return AssignTagDialog( title: 'Edit Device', - addedProducts: TagHelper.createInitialSelectedProductsForTags(tags, subspaces), + addedProducts: TagHelper.createInitialSelectedProductsForTags( + tags, subspaces), spaceName: name, products: products, subspaces: subspaces, @@ -646,7 +449,8 @@ class CreateSpaceDialogState extends State { if (subspaces != null) { for (final subspace in subspaces!) { for (final selectedSubspace in selectedSubspaces) { - if (subspace.subspaceName == selectedSubspace.subspaceName) { + if (subspace.subspaceName == + selectedSubspace.subspaceName) { subspace.tags = selectedSubspace.tags; } } @@ -670,7 +474,8 @@ class CreateSpaceDialogState extends State { allTags: widget.allTags, projectTags: widget.projectTags, initialSelectedProducts: - TagHelper.createInitialSelectedProductsForTags(tags, subspaces), + TagHelper.createInitialSelectedProductsForTags( + tags, subspaces), onSave: (selectedSpaceTags, selectedSubspaces) { setState(() { tags = selectedSpaceTags; @@ -680,7 +485,8 @@ class CreateSpaceDialogState extends State { if (subspaces != null) { for (final subspace in subspaces!) { for (final selectedSubspace in selectedSubspaces) { - if (subspace.subspaceName == selectedSubspace.subspaceName) { + if (subspace.subspaceName == + selectedSubspace.subspaceName) { subspace.tags = selectedSubspace.tags; } } diff --git a/lib/pages/spaces_management/all_spaces/widgets/plus_button_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/plus_button_widget.dart index 40be7284..6c5babaf 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/plus_button_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/plus_button_widget.dart @@ -5,7 +5,7 @@ class PlusButtonWidget extends StatelessWidget { final int index; final String direction; final Offset offset; - final Function(int index, Offset newPosition, String direction) onButtonTap; + final Function(int index, Offset newPosition) onButtonTap; const PlusButtonWidget({ super.key, @@ -17,35 +17,21 @@ class PlusButtonWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return Positioned( - left: offset.dx, - top: offset.dy, - child: GestureDetector( - onTap: () { - Offset newPosition; - switch (direction) { - case 'left': - newPosition = const Offset(-200, 0); - break; - case 'right': - newPosition = const Offset(200, 0); - break; - case 'down': - newPosition = const Offset(0, 150); - break; - default: - newPosition = Offset.zero; - } - onButtonTap(index, newPosition, direction); - }, - child: Container( - width: 30, - height: 30, - decoration: const BoxDecoration( - color: ColorsManager.spaceColor, - shape: BoxShape.circle, - ), - child: const Icon(Icons.add, color: ColorsManager.whiteColors, size: 20), + return GestureDetector( + onTap: () { + onButtonTap(index, const Offset(0, 150)); + }, + child: Container( + width: 30, + height: 30, + decoration: const BoxDecoration( + color: ColorsManager.spaceColor, + shape: BoxShape.circle, + ), + child: const Icon( + Icons.add, + color: ColorsManager.whiteColors, + size: 20, ), ), ); diff --git a/lib/pages/spaces_management/all_spaces/widgets/space_card_widget.dart b/lib/pages/spaces_management/all_spaces/widgets/space_card_widget.dart index f3a476b2..7e6e132f 100644 --- a/lib/pages/spaces_management/all_spaces/widgets/space_card_widget.dart +++ b/lib/pages/spaces_management/all_spaces/widgets/space_card_widget.dart @@ -7,7 +7,7 @@ class SpaceCardWidget extends StatelessWidget { final Offset position; final bool isHovered; final Function(int index, bool isHovered) onHoverChanged; - final Function(int index, Offset newPosition, String direction) onButtonTap; + final Function(int index, Offset newPosition) onButtonTap; final Widget Function(int index) buildSpaceContainer; final ValueChanged onPositionChanged; @@ -25,35 +25,34 @@ class SpaceCardWidget extends StatelessWidget { @override Widget build(BuildContext context) { - return GestureDetector( - behavior: HitTestBehavior.opaque, - onPanUpdate: (details) { - // Call the provided callback to update the position - final newPosition = position + details.delta; - onPositionChanged(newPosition); - }, - child: MouseRegion( - onEnter: (_) { - // Call the provided callback to handle hover state - onHoverChanged(index, true); - }, - onExit: (_) { - // Call the provided callback to handle hover state - onHoverChanged(index, false); - }, + return MouseRegion( + onEnter: (_) => onHoverChanged(index, true), + onExit: (_) => onHoverChanged(index, false), + child: SizedBox( + width: 140, // Make sure this covers both card and plus button + height: 90, child: Stack( - clipBehavior: Clip - .none, // Allow hovering elements to be displayed outside the boundary + clipBehavior: Clip.none, children: [ - buildSpaceContainer(index), // Build the space container - if (isHovered) ...[ - PlusButtonWidget( - index: index, - direction: 'down', - offset: const Offset(63, 50), - onButtonTap: onButtonTap, + // Main card + Container( + width: 140, + height: 80, + alignment: Alignment.center, + color: Colors.transparent, + child: buildSpaceContainer(index), + ), + // Plus button (NO inner Positioned!) + if (isHovered) + Align( + alignment: Alignment.bottomCenter, + child: PlusButtonWidget( + index: index, + direction: 'down', + offset: Offset.zero, + onButtonTap: onButtonTap, + ), ), - ], ], ), ), diff --git a/lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart b/lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart index 74161b6f..afc0c852 100644 --- a/lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart +++ b/lib/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart @@ -13,7 +13,8 @@ class AssignTagBloc extends Bloc { final existingTagCounts = {}; for (var tag in initialTags) { if (tag.product != null) { - existingTagCounts[tag.product!.uuid] = (existingTagCounts[tag.product!.uuid] ?? 0) + 1; + existingTagCounts[tag.product!.uuid] = + (existingTagCounts[tag.product!.uuid] ?? 0) + 1; } } @@ -22,14 +23,17 @@ class AssignTagBloc extends Bloc { for (var selectedProduct in event.addedProducts) { final existingCount = existingTagCounts[selectedProduct.productId] ?? 0; - if (selectedProduct.count == 0 || selectedProduct.count <= existingCount) { - tags.addAll(initialTags.where((tag) => tag.product?.uuid == selectedProduct.productId)); + if (selectedProduct.count == 0 || + selectedProduct.count <= existingCount) { + tags.addAll(initialTags + .where((tag) => tag.product?.uuid == selectedProduct.productId)); continue; } final missingCount = selectedProduct.count - existingCount; - tags.addAll(initialTags.where((tag) => tag.product?.uuid == selectedProduct.productId)); + tags.addAll(initialTags + .where((tag) => tag.product?.uuid == selectedProduct.productId)); if (missingCount > 0) { tags.addAll(List.generate( @@ -85,7 +89,8 @@ class AssignTagBloc extends Bloc { final tags = List.from(currentState.tags); // Update the location - tags[event.index] = tags[event.index].copyWith(location: event.location); + tags[event.index] = + tags[event.index].copyWith(location: event.location); final updatedTags = _calculateAvailableTags(projectTags, tags); @@ -117,7 +122,8 @@ class AssignTagBloc extends Bloc { final currentState = state; if (currentState is AssignTagLoaded && currentState.tags.isNotEmpty) { - final tags = List.from(currentState.tags)..remove(event.tagToDelete); + final tags = List.from(currentState.tags) + ..remove(event.tagToDelete); // Recalculate available tags final updatedTags = _calculateAvailableTags(projectTags, tags); @@ -141,8 +147,10 @@ class AssignTagBloc extends Bloc { // Get validation error for duplicate tags String? _getValidationError(List tags) { - final nonEmptyTags = - tags.map((tag) => tag.tag?.trim() ?? '').where((tag) => tag.isNotEmpty).toList(); + final nonEmptyTags = tags + .map((tag) => tag.tag?.trim() ?? '') + .where((tag) => tag.isNotEmpty) + .toList(); final duplicateTags = nonEmptyTags .fold>({}, (map, tag) { @@ -168,9 +176,11 @@ class AssignTagBloc extends Bloc { .toSet(); final availableTags = allTags - .where((tag) => tag.tag != null && !selectedTagSet.contains(tag.tag!.trim())) + .where((tag) => + tag.tag != null && !selectedTagSet.contains(tag.tag!.trim())) .toList(); - return availableTags; + return projectTags; + // availableTags; } } diff --git a/lib/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart b/lib/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart index fd1454e5..21ba141c 100644 --- a/lib/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart +++ b/lib/pages/spaces_management/assign_tag/views/assign_tag_dialog.dart @@ -1,10 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/common/dialog_dropdown.dart'; -import 'package:syncrow_web/common/tag_dialog_textfield_dropdown.dart'; -import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; -import 'package:syncrow_web/pages/spaces_management/add_device_type/views/add_device_type_widget.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; @@ -12,9 +7,10 @@ import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_event.dart'; import 'package:syncrow_web/pages/spaces_management/assign_tag/bloc/assign_tag_state.dart'; -import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart'; +import 'package:syncrow_web/pages/spaces_management/assign_tag_models/views/widgets/assign_tags_tables_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:uuid/uuid.dart'; + +import 'widgets/save_add_device_row_widget.dart'; class AssignTagDialog extends StatelessWidget { final List? products; @@ -29,7 +25,7 @@ class AssignTagDialog extends StatelessWidget { final List projectTags; const AssignTagDialog( - {Key? key, + {super.key, required this.products, required this.subspaces, required this.addedProducts, @@ -39,13 +35,14 @@ class AssignTagDialog extends StatelessWidget { required this.spaceName, required this.title, this.onSave, - required this.projectTags}) - : super(key: key); + required this.projectTags}); @override Widget build(BuildContext context) { - final List locations = - (subspaces ?? []).map((subspace) => subspace.subspaceName).toList()..add('Main Space'); + final List locations = (subspaces ?? []) + .map((subspace) => subspace.subspaceName) + .toList() + ..add('Main Space'); return BlocProvider( create: (_) => AssignTagBloc(projectTags) @@ -67,131 +64,31 @@ class AssignTagDialog extends StatelessWidget { content: SingleChildScrollView( child: Column( children: [ - ClipRRect( - borderRadius: BorderRadius.circular(20), - child: DataTable( - headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey), - key: ValueKey(state.tags.length), - border: TableBorder.all( - color: ColorsManager.dataHeaderGrey, - width: 1, - borderRadius: BorderRadius.circular(20), - ), - columns: [ - DataColumn( - label: Text('#', style: Theme.of(context).textTheme.bodyMedium)), - DataColumn( - label: Text('Device', style: Theme.of(context).textTheme.bodyMedium)), - DataColumn( - numeric: false, - label: Text('Tag', style: Theme.of(context).textTheme.bodyMedium)), - DataColumn( - label: - Text('Location', style: Theme.of(context).textTheme.bodyMedium)), - ], - rows: state.tags.isEmpty - ? [ - DataRow(cells: [ - DataCell( - Center( - child: Text('No Data Available', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: ColorsManager.lightGrayColor, - )), - ), - ), - const DataCell(SizedBox()), - const DataCell(SizedBox()), - const DataCell(SizedBox()), - ]) - ] - : List.generate(state.tags.length, (index) { - final tag = state.tags[index]; - final controller = controllers[index]; + AssignTagsTable( + controllers: controllers, + locations: locations, + tags: state.tags, + updatedTags: state.updatedTags, + onDeleteDevice: ({required index, required tag}) { + context + .read() + .add(DeleteTag(tagToDelete: tag, tags: state.tags)); - return DataRow( - cells: [ - DataCell(Text((index + 1).toString())), - DataCell( - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - tag.product?.name ?? 'Unknown', - overflow: TextOverflow.ellipsis, - )), - const SizedBox(width: 10), - Container( - width: 20.0, - height: 20.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: ColorsManager.lightGrayColor, - width: 1.0, - ), - ), - child: IconButton( - icon: const Icon( - Icons.close, - color: ColorsManager.lightGreyColor, - size: 16, - ), - onPressed: () { - context.read().add( - DeleteTag(tagToDelete: tag, tags: state.tags)); - - controllers.removeAt(index); - }, - tooltip: 'Delete Tag', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ), - ], - ), - ), - DataCell( - Container( - alignment: - Alignment.centerLeft, // Align cell content to the left - child: SizedBox( - width: double.infinity, - child: TagDialogTextfieldDropdown( - key: ValueKey('dropdown_${const Uuid().v4()}_$index'), - items: state.updatedTags, - product: tag.product?.uuid ?? 'Unknown', - initialValue: tag, - onSelected: (value) { - controller.text = value.tag ?? ''; - context.read().add(UpdateTagEvent( - index: index, - tag: value, - )); - }, - ), - ), - ), - ), - DataCell( - SizedBox( - width: double.infinity, - child: DialogDropdown( - items: locations, - selectedValue: tag.location ?? 'Main Space', - onSelected: (value) { - context.read().add(UpdateLocation( - index: index, - location: value, - )); - }, - )), - ), - ], - ); - }), - ), + controllers.removeAt(index); + }, + onTagDropDownSelected: ({required index, required tag}) { + context.read().add(UpdateTagEvent( + index: index, + tag: tag, + )); + }, + onLocationDropDownSelected: ( + {required index, required location}) { + context.read().add(UpdateLocation( + index: index, + location: location, + )); + }, ), if (state.errorMessage != null) Text(state.errorMessage!, @@ -203,69 +100,15 @@ class AssignTagDialog extends StatelessWidget { ), ), actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - const SizedBox(width: 10), - Expanded( - child: Builder( - builder: (buttonContext) => CancelButton( - label: 'Add New Device', - onPressed: () async { - final updatedTags = List.from(state.tags); - final result = TagHelper.processTags(updatedTags, subspaces); - - final processedTags = result['updatedTags'] as List; - final processedSubspaces = - List.from(result['subspaces'] as List); - - Navigator.of(context).pop(); - - await showDialog( - context: context, - builder: (context) => AddDeviceTypeWidget( - products: products, - subspaces: processedSubspaces, - projectTags: projectTags, - initialSelectedProducts: - TagHelper.createInitialSelectedProductsForTags( - processedTags, processedSubspaces), - spaceName: spaceName, - spaceTags: processedTags, - isCreate: false, - onSave: onSave, - allTags: allTags, - ), - ); - }, - ), - ), - ), - const SizedBox(width: 10), - Expanded( - child: DefaultButton( - borderRadius: 10, - backgroundColor: ColorsManager.secondaryColor, - foregroundColor: state.isSaveEnabled - ? ColorsManager.whiteColors - : ColorsManager.whiteColorsWithOpacity, - onPressed: state.isSaveEnabled - ? () async { - final updatedTags = List.from(state.tags); - final result = TagHelper.processTags(updatedTags, subspaces); - - final processedTags = result['updatedTags'] as List; - final processedSubspaces = - List.from(result['subspaces'] as List); - onSave?.call(processedTags, processedSubspaces); - Navigator.of(context).pop(); - } - : null, - child: const Text('Save'), - ), - ), - const SizedBox(width: 10), - ], + SaveAddDeviceRowWidget( + subspaces: subspaces, + products: products, + projectTags: projectTags, + spaceName: spaceName, + onSave: onSave, + allTags: allTags, + tags: state.tags, + isSaveEnabled: state.isSaveEnabled, ), ], ); diff --git a/lib/pages/spaces_management/assign_tag/views/widgets/save_add_device_row_widget.dart b/lib/pages/spaces_management/assign_tag/views/widgets/save_add_device_row_widget.dart new file mode 100644 index 00000000..a81b7ad0 --- /dev/null +++ b/lib/pages/spaces_management/assign_tag/views/widgets/save_add_device_row_widget.dart @@ -0,0 +1,100 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; + +import '../../../add_device_type/views/add_device_type_widget.dart'; +import '../../../helper/tag_helper.dart'; + +class SaveAddDeviceRowWidget extends StatelessWidget { + const SaveAddDeviceRowWidget({ + super.key, + required this.subspaces, + required this.products, + required this.projectTags, + required this.spaceName, + required this.onSave, + required this.allTags, + required this.tags, + required this.isSaveEnabled, + }); + final List tags; + final List? subspaces; + final List? products; + final List projectTags; + final String spaceName; + final Function(List p1, List? p2)? onSave; + final List? allTags; + final bool isSaveEnabled; + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const SizedBox(width: 10), + Expanded( + child: Builder( + builder: (buttonContext) => CancelButton( + label: 'Add New Device', + onPressed: () async { + final updatedTags = List.from(tags); + final result = TagHelper.processTags(updatedTags, subspaces); + + final processedTags = result['updatedTags'] as List; + final processedSubspaces = List.from( + result['subspaces'] as List); + + Navigator.of(context).pop(); + + await showDialog( + context: context, + builder: (context) => AddDeviceTypeWidget( + products: products, + subspaces: processedSubspaces, + projectTags: projectTags, + initialSelectedProducts: + TagHelper.createInitialSelectedProductsForTags( + processedTags, processedSubspaces), + spaceName: spaceName, + spaceTags: processedTags, + isCreate: false, + onSave: onSave, + allTags: allTags, + ), + ); + }, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: DefaultButton( + borderRadius: 10, + backgroundColor: ColorsManager.secondaryColor, + foregroundColor: isSaveEnabled + ? ColorsManager.whiteColors + : ColorsManager.whiteColorsWithOpacity, + onPressed: isSaveEnabled + ? () async { + final updatedTags = List.from(tags); + final result = + TagHelper.processTags(updatedTags, subspaces); + + final processedTags = result['updatedTags'] as List; + final processedSubspaces = List.from( + result['subspaces'] as List); + onSave?.call(processedTags, processedSubspaces); + Navigator.of(context).pop(); + } + : null, + child: const Text('Save'), + ), + ), + const SizedBox(width: 10), + ], + ); + } +} diff --git a/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart b/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart index 85be3bf3..57ed93df 100644 --- a/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart +++ b/lib/pages/spaces_management/assign_tag_models/views/assign_tag_models_dialog.dart @@ -1,9 +1,5 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/common/dialog_dropdown.dart'; -import 'package:syncrow_web/common/tag_dialog_textfield_dropdown.dart'; -import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/selected_product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; @@ -12,11 +8,10 @@ import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assig import 'package:syncrow_web/pages/spaces_management/assign_tag_models/bloc/assign_tag_model_state.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; -import 'package:syncrow_web/pages/spaces_management/space_model/widgets/dialog/create_space_model_dialog.dart'; -import 'package:syncrow_web/pages/spaces_management/tag_model/views/add_device_type_model_widget.dart'; import 'package:syncrow_web/utils/color_manager.dart'; -import 'package:syncrow_web/pages/spaces_management/helper/tag_helper.dart'; -import 'package:uuid/uuid.dart'; + +import 'widgets/RowOfCancelSaveWidget.dart'; +import 'widgets/assign_tags_tables_widget.dart'; class AssignTagModelsDialog extends StatelessWidget { final List? products; @@ -53,8 +48,10 @@ class AssignTagModelsDialog extends StatelessWidget { @override Widget build(BuildContext context) { - final List locations = - (subspaces ?? []).map((subspace) => subspace.subspaceName).toList()..add('Main Space'); + final List locations = (subspaces ?? []) + .map((subspace) => subspace.subspaceName) + .toList() + ..add('Main Space'); return BlocProvider( create: (_) => AssignTagModelBloc(projectTags) @@ -78,137 +75,38 @@ class AssignTagModelsDialog extends StatelessWidget { content: SingleChildScrollView( child: Column( children: [ - ClipRRect( - borderRadius: BorderRadius.circular(20), - child: DataTable( - headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey), - key: ValueKey(state.tags.length), - border: TableBorder.all( - color: ColorsManager.dataHeaderGrey, - width: 1, - borderRadius: BorderRadius.circular(20), - ), - columns: [ - DataColumn( - label: Text('#', style: Theme.of(context).textTheme.bodyMedium)), - DataColumn( - label: Text('Device', - style: Theme.of(context).textTheme.bodyMedium)), - DataColumn( - numeric: false, - label: - Text('Tag', style: Theme.of(context).textTheme.bodyMedium)), - DataColumn( - label: Text('Location', - style: Theme.of(context).textTheme.bodyMedium)), - ], - rows: state.tags.isEmpty - ? [ - DataRow(cells: [ - DataCell( - Center( - child: Text('No Devices Available', - style: - Theme.of(context).textTheme.bodyMedium?.copyWith( - color: ColorsManager.lightGrayColor, - )), - ), - ), - const DataCell(SizedBox()), - const DataCell(SizedBox()), - const DataCell(SizedBox()), - ]) - ] - : List.generate(state.tags.length, (index) { - final tag = state.tags[index]; - final controller = controllers[index]; - - return DataRow( - cells: [ - DataCell(Text((index + 1).toString())), - DataCell( - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - tag.product?.name ?? 'Unknown', - overflow: TextOverflow.ellipsis, - )), - const SizedBox(width: 10), - Container( - width: 20.0, - height: 20.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all( - color: ColorsManager.lightGrayColor, - width: 1.0, - ), - ), - child: IconButton( - icon: const Icon( - Icons.close, - color: ColorsManager.lightGreyColor, - size: 16, - ), - onPressed: () { - context.read().add( - DeleteTagModel( - tagToDelete: tag, tags: state.tags)); - controllers.removeAt(index); - }, - tooltip: 'Delete Tag', - padding: EdgeInsets.zero, - constraints: const BoxConstraints(), - ), - ), - ], - ), - ), - DataCell( - Container( - alignment: Alignment - .centerLeft, // Align cell content to the left - child: SizedBox( - width: double.infinity, - child: TagDialogTextfieldDropdown( - key: ValueKey( - 'dropdown_${const Uuid().v4()}_$index'), - product: tag.product?.uuid ?? 'Unknown', - items: state.updatedTags, - initialValue: tag, - onSelected: (value) { - controller.text = value.tag ?? ''; - context.read().add(UpdateTag( - index: index, - tag: value, - )); - }, - ), - ), - ), - ), - DataCell( - SizedBox( - width: double.infinity, - child: DialogDropdown( - items: locations, - selectedValue: tag.location ?? 'Main Space', - onSelected: (value) { - context - .read() - .add(UpdateLocation( - index: index, - location: value, - )); - }, - )), - ), - ], - ); - }), - ), + AssignTagsTable( + controllers: controllers, + locations: locations, + tags: state.tags, + updatedTags: state.updatedTags, + onDeleteDevice: ({required index, required tag}) { + context + .read() + .add(DeleteTagModel( + tagToDelete: tag, + tags: state.tags, + )); + controllers.removeAt(index); + }, + onTagDropDownSelected: ( + {required index, required tag}) { + context.read().add( + UpdateTag( + index: index, + tag: tag, + ), + ); + }, + onLocationDropDownSelected: ( + {required index, required location}) { + context.read().add( + UpdateLocation( + index: index, + location: location, + ), + ); + }, ), if (state.errorMessage != null) Text(state.errorMessage!, @@ -220,101 +118,16 @@ class AssignTagModelsDialog extends StatelessWidget { ), ), actions: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceAround, - children: [ - const SizedBox(width: 10), - Expanded( - child: Builder( - builder: (buttonContext) => CancelButton( - label: 'Add New Device', - onPressed: () async { - final updatedTags = List.from(state.tags); - final result = - TagHelper.updateSubspaceTagModels(updatedTags, subspaces); - - final processedTags = result['updatedTags'] as List; - final processedSubspaces = List.from( - result['subspaces'] as List); - - if (context.mounted) { - Navigator.of(context).pop(); - - await showDialog( - barrierDismissible: false, - context: context, - builder: (dialogContext) => AddDeviceTypeModelWidget( - products: products, - subspaces: processedSubspaces, - isCreate: false, - initialSelectedProducts: - TagHelper.createInitialSelectedProducts( - processedTags, processedSubspaces), - allTags: allTags, - spaceName: spaceName, - otherSpaceModels: otherSpaceModels, - spaceTagModels: processedTags, - pageContext: pageContext, - projectTags: projectTags, - spaceModel: SpaceTemplateModel( - modelName: spaceName, - tags: updatedTags, - uuid: spaceModel?.uuid, - internalId: spaceModel?.internalId, - subspaceModels: processedSubspaces)), - ); - } - }, - ), - ), - ), - const SizedBox(width: 10), - Expanded( - child: DefaultButton( - borderRadius: 10, - backgroundColor: ColorsManager.secondaryColor, - foregroundColor: state.isSaveEnabled - ? ColorsManager.whiteColors - : ColorsManager.whiteColorsWithOpacity, - onPressed: state.isSaveEnabled - ? () async { - final updatedTags = List.from(state.tags); - - final result = - TagHelper.updateSubspaceTagModels(updatedTags, subspaces); - - final processedTags = result['updatedTags'] as List; - final processedSubspaces = List.from( - result['subspaces'] as List); - - Navigator.of(context).popUntil((route) => route.isFirst); - - await showDialog( - context: context, - builder: (BuildContext dialogContext) { - return CreateSpaceModelDialog( - products: products, - allSpaceModels: allSpaceModels, - allTags: allTags, - projectTags: projectTags, - pageContext: pageContext, - otherSpaceModels: otherSpaceModels, - spaceModel: SpaceTemplateModel( - modelName: spaceName, - tags: processedTags, - uuid: spaceModel?.uuid, - internalId: spaceModel?.internalId, - subspaceModels: processedSubspaces), - ); - }, - ); - } - : null, - child: const Text('Save'), - ), - ), - const SizedBox(width: 10), - ], + RowOfSaveCancelWidget( + subspaces: subspaces, + products: products, + allTags: allTags, + spaceName: spaceName, + otherSpaceModels: otherSpaceModels, + pageContext: pageContext, + projectTags: projectTags, + spaceModel: spaceModel, + allSpaceModels: allSpaceModels, ), ], ); diff --git a/lib/pages/spaces_management/assign_tag_models/views/widgets/RowOfCancelSaveWidget.dart b/lib/pages/spaces_management/assign_tag_models/views/widgets/RowOfCancelSaveWidget.dart new file mode 100644 index 00000000..9b2a1367 --- /dev/null +++ b/lib/pages/spaces_management/assign_tag_models/views/widgets/RowOfCancelSaveWidget.dart @@ -0,0 +1,152 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../../../utils/color_manager.dart'; +import '../../../../common/buttons/cancel_button.dart'; +import '../../../../common/buttons/default_button.dart'; +import '../../../all_spaces/model/product_model.dart'; +import '../../../all_spaces/model/tag.dart'; +import '../../../helper/tag_helper.dart'; +import '../../../space_model/models/space_template_model.dart'; +import '../../../space_model/models/subspace_template_model.dart'; +import '../../../space_model/widgets/dialog/create_space_model_dialog.dart'; +import '../../../tag_model/views/add_device_type_model_widget.dart'; +import '../../bloc/assign_tag_model_bloc.dart'; +import '../../bloc/assign_tag_model_state.dart'; + +class RowOfSaveCancelWidget extends StatelessWidget { + const RowOfSaveCancelWidget({ + super.key, + required this.subspaces, + required this.products, + required this.allTags, + required this.spaceName, + required this.otherSpaceModels, + required this.pageContext, + required this.projectTags, + required this.spaceModel, + required this.allSpaceModels, + }); + + final List? subspaces; + final List? products; + final List? allTags; + final String spaceName; + final List? otherSpaceModels; + final BuildContext? pageContext; + final List projectTags; + final SpaceTemplateModel? spaceModel; + final List? allSpaceModels; + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + if (state is AssignTagModelLoaded) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceAround, + children: [ + const SizedBox(width: 10), + Expanded( + child: Builder( + builder: (buttonContext) => CancelButton( + label: 'Add New Device', + onPressed: () async { + final updatedTags = List.from(state.tags); + final result = TagHelper.updateSubspaceTagModels( + updatedTags, subspaces); + + final processedTags = result['updatedTags'] as List; + final processedSubspaces = + List.from( + result['subspaces'] as List); + + if (context.mounted) { + Navigator.of(context).pop(); + + await showDialog( + barrierDismissible: false, + context: context, + builder: (dialogContext) => AddDeviceTypeModelWidget( + products: products, + subspaces: processedSubspaces, + isCreate: false, + initialSelectedProducts: + TagHelper.createInitialSelectedProducts( + processedTags, processedSubspaces), + allTags: allTags, + spaceName: spaceName, + otherSpaceModels: otherSpaceModels, + spaceTagModels: processedTags, + pageContext: pageContext, + projectTags: projectTags, + spaceModel: SpaceTemplateModel( + modelName: spaceName, + tags: updatedTags, + uuid: spaceModel?.uuid, + internalId: spaceModel?.internalId, + subspaceModels: processedSubspaces)), + ); + } + }, + ), + ), + ), + const SizedBox(width: 10), + Expanded( + child: DefaultButton( + borderRadius: 10, + backgroundColor: ColorsManager.secondaryColor, + foregroundColor: state.isSaveEnabled + ? ColorsManager.whiteColors + : ColorsManager.whiteColorsWithOpacity, + onPressed: state.isSaveEnabled + ? () async { + final updatedTags = List.from(state.tags); + + final result = TagHelper.updateSubspaceTagModels( + updatedTags, subspaces); + + final processedTags = + result['updatedTags'] as List; + final processedSubspaces = + List.from( + result['subspaces'] as List); + + Navigator.of(context) + .popUntil((route) => route.isFirst); + + await showDialog( + context: context, + builder: (BuildContext dialogContext) { + return CreateSpaceModelDialog( + products: products, + allSpaceModels: allSpaceModels, + allTags: allTags, + projectTags: projectTags, + pageContext: pageContext, + otherSpaceModels: otherSpaceModels, + spaceModel: SpaceTemplateModel( + modelName: spaceName, + tags: processedTags, + uuid: spaceModel?.uuid, + internalId: spaceModel?.internalId, + subspaceModels: processedSubspaces), + ); + }, + ); + } + : null, + child: const Text('Save'), + ), + ), + const SizedBox(width: 10), + ], + ); + } else { + return const SizedBox(); + } + }, + ); + } +} diff --git a/lib/pages/spaces_management/assign_tag_models/views/widgets/assign_tags_tables_widget.dart b/lib/pages/spaces_management/assign_tag_models/views/widgets/assign_tags_tables_widget.dart new file mode 100644 index 00000000..0e17a0d7 --- /dev/null +++ b/lib/pages/spaces_management/assign_tag_models/views/widgets/assign_tags_tables_widget.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; +import 'package:uuid/uuid.dart'; + +import '../../../../../common/dialog_dropdown.dart'; +import '../../../../../common/tag_dialog_textfield_dropdown.dart'; +import '../../../../../utils/color_manager.dart'; + +class AssignTagsTable extends StatelessWidget { + const AssignTagsTable({ + super.key, + required this.controllers, + required this.locations, + required this.tags, + required this.updatedTags, + required this.onDeleteDevice, + required this.onLocationDropDownSelected, + required this.onTagDropDownSelected, + }); + final void Function({required Tag tag, required int index}) + onTagDropDownSelected; + final void Function({required String location, required int index}) + onLocationDropDownSelected; + final void Function({required Tag tag, required int index}) onDeleteDevice; + final List tags; + final List updatedTags; + final List controllers; + final List locations; + + @override + Widget build(BuildContext context) { + return ClipRRect( + borderRadius: BorderRadius.circular(20), + child: DataTable( + headingRowColor: WidgetStateProperty.all(ColorsManager.dataHeaderGrey), + key: ValueKey(tags.length), + border: TableBorder.all( + color: ColorsManager.dataHeaderGrey, + width: 1, + borderRadius: BorderRadius.circular(20), + ), + columns: [ + DataColumn( + label: Text('#', style: Theme.of(context).textTheme.bodyMedium)), + DataColumn( + label: Text('Device', + style: Theme.of(context).textTheme.bodyMedium)), + DataColumn( + numeric: false, + label: + Text('Tag', style: Theme.of(context).textTheme.bodyMedium)), + DataColumn( + label: Text('Location', + style: Theme.of(context).textTheme.bodyMedium)), + ], + rows: tags.isEmpty + ? [ + DataRow(cells: [ + DataCell( + Center( + child: Text('No Devices Available', + style: + Theme.of(context).textTheme.bodyMedium?.copyWith( + color: ColorsManager.lightGrayColor, + )), + ), + ), + const DataCell(SizedBox()), + const DataCell(SizedBox()), + const DataCell(SizedBox()), + ]) + ] + : List.generate(tags.length, (index) { + final tag = tags[index]; + final controller = controllers[index]; + + return DataRow( + cells: [ + DataCell(Text((index + 1).toString())), + DataCell( + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + tag.product?.name ?? 'Unknown', + overflow: TextOverflow.ellipsis, + )), + const SizedBox(width: 10), + Container( + width: 20.0, + height: 20.0, + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: ColorsManager.lightGrayColor, + width: 1.0, + ), + ), + child: IconButton( + icon: const Icon( + Icons.close, + color: ColorsManager.lightGreyColor, + size: 16, + ), + onPressed: () { + onDeleteDevice(tag: tag, index: index); + }, + tooltip: 'Delete Tag', + padding: EdgeInsets.zero, + constraints: const BoxConstraints(), + ), + ), + ], + ), + ), + DataCell( + Container( + alignment: Alignment + .centerLeft, // Align cell content to the left + child: SizedBox( + width: double.infinity, + child: TagDialogTextfieldDropdown( + key: ValueKey( + 'dropdown_${const Uuid().v4()}_$index'), + product: tag.product?.uuid ?? 'Unknown', + items: updatedTags, + initialValue: tag, + onSelected: (value) { + controller.text = value.tag ?? ''; + onTagDropDownSelected(tag: value, index: index); + }, + ), + ), + ), + ), + DataCell( + SizedBox( + width: double.infinity, + child: DialogDropdown( + items: locations, + selectedValue: tag.location ?? 'Main Space', + onSelected: (value) { + onLocationDropDownSelected( + location: value, index: index); + }, + )), + ), + ], + ); + }), + ), + ); + } +} diff --git a/lib/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart b/lib/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart index b334a301..a2d44553 100644 --- a/lib/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart +++ b/lib/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart @@ -77,7 +77,7 @@ class SubSpaceBloc extends Bloc { emit(SubSpaceState( updatedSubSpaces, updatedSubspaceModels, - errorMessage, + errorMessage , updatedDuplicates, )); }); diff --git a/lib/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart b/lib/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart index 948028ed..82df866a 100644 --- a/lib/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart +++ b/lib/pages/spaces_management/create_subspace/views/create_subspace_model_dialog.dart @@ -1,23 +1,23 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; -import 'package:syncrow_web/pages/common/buttons/default_button.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/product_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_bloc.dart'; import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_event.dart'; import 'package:syncrow_web/pages/spaces_management/create_subspace/bloc/subspace_state.dart'; -import 'package:syncrow_web/pages/spaces_management/create_subspace_model/widgets/subspace_chip.dart'; -import 'package:syncrow_web/pages/spaces_management/space_model/models/subspace_template_model.dart'; import 'package:syncrow_web/utils/color_manager.dart'; import 'package:syncrow_web/utils/extension/build_context_x.dart'; +import 'widgets/ok_cancel_sub_space_widget.dart'; +import 'widgets/textfield_sub_space_dialog_widget.dart'; + class CreateSubSpaceDialog extends StatefulWidget { final String dialogTitle; final List? existingSubSpaces; final String? spaceName; final List? products; - final void Function(List?)? onSave; + final void Function( + List?, List updatedSubSpaces)? onSave; const CreateSubSpaceDialog({ required this.dialogTitle, @@ -33,14 +33,7 @@ class CreateSubSpaceDialog extends StatefulWidget { } class _CreateSubSpaceDialogState extends State { - late final TextEditingController _subspaceNameController; - - @override - void initState() { - _subspaceNameController = TextEditingController(); - super.initState(); - } - + final TextEditingController _subspaceNameController = TextEditingController(); @override void dispose() { _subspaceNameController.dispose(); @@ -79,83 +72,26 @@ class _CreateSubSpaceDialogState extends State { color: ColorsManager.blackColor, ), ), + Row( + children: [ + Text( + 'press Enter to Save', + style: context.textTheme.headlineLarge?.copyWith( + fontWeight: FontWeight.w500, + fontSize: 10, + color: ColorsManager.grayColor, + ), + ), + const SizedBox( + width: 5, + ), + const Icon(Icons.save_as_sharp, size: 10), + ], + ), const SizedBox(height: 16), - Container( - width: context.screenWidth * 0.35, - padding: const EdgeInsets.symmetric( - vertical: 10, - horizontal: 16, - ), - decoration: BoxDecoration( - color: ColorsManager.boxColor, - borderRadius: BorderRadius.circular(10), - ), - child: Wrap( - spacing: 8, - runSpacing: 8, - alignment: WrapAlignment.start, - crossAxisAlignment: WrapCrossAlignment.center, - children: [ - ...state.subSpaces.asMap().entries.map( - (entry) { - final index = entry.key; - final subSpace = entry.value; - - final lowerName = subSpace.subspaceName.toLowerCase(); - - final duplicateIndices = state.subSpaces - .asMap() - .entries - .where((e) => - e.value.subspaceName.toLowerCase() == lowerName) - .map((e) => e.key) - .toList(); - final isDuplicate = duplicateIndices.length > 1 && - duplicateIndices.indexOf(index) != 0; - return SubspaceChip( - subSpace: SubspaceTemplateModel( - subspaceName: entry.value.subspaceName, - disabled: entry.value.disabled, - ), - isDuplicate: isDuplicate, - onDeleted: () => context.read().add( - RemoveSubSpace(subSpace), - ), - ); - }, - ), - SizedBox( - width: 200, - child: TextField( - controller: _subspaceNameController, - decoration: InputDecoration( - border: InputBorder.none, - hintText: state.subSpaces.isEmpty - ? 'Please enter the name' - : null, - hintStyle: context.textTheme.bodySmall?.copyWith( - color: ColorsManager.lightGrayColor, - ), - ), - onSubmitted: (value) { - final trimmedValue = value.trim(); - if (trimmedValue.isNotEmpty) { - context.read().add( - AddSubSpace( - SubspaceModel( - subspaceName: trimmedValue, - disabled: false, - ), - ), - ); - _subspaceNameController.clear(); - } - }, - style: context.textTheme.bodyMedium, - ), - ), - ], - ), + TextFieldSubSpaceDialogWidget( + subspaceNameController: _subspaceNameController, + subSpaces: state.subSpaces, ), if (state.errorMessage.isNotEmpty) Padding( @@ -168,36 +104,10 @@ class _CreateSubSpaceDialogState extends State { ), ), const SizedBox(height: 16), - Row( - children: [ - Expanded( - child: CancelButton( - label: 'Cancel', - onPressed: () async { - Navigator.of(context).pop(); - }, - ), - ), - const SizedBox(width: 10), - Expanded( - child: DefaultButton( - onPressed: state.errorMessage.isEmpty - ? () { - final subSpacesBloc = context.read(); - final subSpaces = subSpacesBloc.state.subSpaces; - widget.onSave?.call(subSpaces); - Navigator.of(context).pop(); - } - : null, - backgroundColor: ColorsManager.secondaryColor, - borderRadius: 10, - foregroundColor: state.errorMessage.isNotEmpty - ? ColorsManager.whiteColorsWithOpacity - : ColorsManager.whiteColors, - child: const Text('OK'), - ), - ), - ], + OkCancelSubSpaceWidget( + subspaceNameController: _subspaceNameController, + errorMessage: state.errorMessage, + onSave: widget.onSave, ), ], ), diff --git a/lib/pages/spaces_management/create_subspace/views/widgets/ok_cancel_sub_space_widget.dart b/lib/pages/spaces_management/create_subspace/views/widgets/ok_cancel_sub_space_widget.dart new file mode 100644 index 00000000..3952e105 --- /dev/null +++ b/lib/pages/spaces_management/create_subspace/views/widgets/ok_cancel_sub_space_widget.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/subspace_model.dart'; +import 'package:syncrow_web/utils/color_manager.dart'; +import 'package:syncrow_web/pages/common/buttons/cancel_button.dart'; +import 'package:syncrow_web/pages/common/buttons/default_button.dart'; + +import '../../bloc/subspace_bloc.dart'; +import '../../bloc/subspace_event.dart'; + +class OkCancelSubSpaceWidget extends StatelessWidget { + const OkCancelSubSpaceWidget({ + super.key, + required this.subspaceNameController, + required this.onSave, + required this.errorMessage, + }); + + final TextEditingController subspaceNameController; + final void Function( + List?, List updatedSubSpaces)? onSave; + final String errorMessage; + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: CancelButton( + label: 'Cancel', + onPressed: () async { + Navigator.of(context).pop(); + }, + ), + ), + const SizedBox(width: 10), + Expanded( + child: DefaultButton( + onPressed: errorMessage.isEmpty + ? () async { + final trimmedValue = subspaceNameController.text.trim(); + + final subSpacesBloc = context.read(); + if (trimmedValue.isNotEmpty) { + subSpacesBloc.add( + AddSubSpace( + SubspaceModel( + subspaceName: trimmedValue, + disabled: false, + ), + ), + ); + subspaceNameController.clear(); + } + + await Future.delayed(const Duration(milliseconds: 10)); + final subSpaces = subSpacesBloc.state.subSpaces; + + onSave?.call( + subSpaces, subSpacesBloc.state.updatedSubSpaceModels); + + Navigator.of(context).pop(); + } + : null, + backgroundColor: ColorsManager.secondaryColor, + borderRadius: 10, + foregroundColor: errorMessage.isNotEmpty + ? ColorsManager.whiteColorsWithOpacity + : ColorsManager.whiteColors, + child: const Text('OK'), + ), + ), + ], + ); + } +} diff --git a/lib/pages/spaces_management/create_subspace/views/widgets/textfield_sub_space_dialog_widget.dart b/lib/pages/spaces_management/create_subspace/views/widgets/textfield_sub_space_dialog_widget.dart new file mode 100644 index 00000000..f655e178 --- /dev/null +++ b/lib/pages/spaces_management/create_subspace/views/widgets/textfield_sub_space_dialog_widget.dart @@ -0,0 +1,98 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:syncrow_web/utils/extension/build_context_x.dart'; + +import '../../../../../utils/color_manager.dart'; +import '../../../all_spaces/model/subspace_model.dart'; +import '../../../create_subspace_model/widgets/subspace_chip.dart'; +import '../../../space_model/models/subspace_template_model.dart'; +import '../../bloc/subspace_bloc.dart'; +import '../../bloc/subspace_event.dart'; + +class TextFieldSubSpaceDialogWidget extends StatelessWidget { + const TextFieldSubSpaceDialogWidget({ + super.key, + required TextEditingController subspaceNameController, + required this.subSpaces, + }) : _subspaceNameController = subspaceNameController; + + final TextEditingController _subspaceNameController; + final List subSpaces; + @override + Widget build(BuildContext context) { + return Container( + width: context.screenWidth * 0.35, + padding: const EdgeInsets.symmetric( + vertical: 10, + horizontal: 16, + ), + decoration: BoxDecoration( + color: ColorsManager.boxColor, + borderRadius: BorderRadius.circular(10), + ), + child: Wrap( + spacing: 8, + runSpacing: 8, + alignment: WrapAlignment.start, + crossAxisAlignment: WrapCrossAlignment.center, + children: [ + ...subSpaces.asMap().entries.map( + (entry) { + final index = entry.key; + final subSpace = entry.value; + + final lowerName = subSpace.subspaceName.toLowerCase(); + + final duplicateIndices = subSpaces + .asMap() + .entries + .where((e) => e.value.subspaceName.toLowerCase() == lowerName) + .map((e) => e.key) + .toList(); + final isDuplicate = duplicateIndices.length > 1 && + duplicateIndices.indexOf(index) != 0; + return SubspaceChip( + subSpace: SubspaceTemplateModel( + subspaceName: entry.value.subspaceName, + disabled: entry.value.disabled, + ), + isDuplicate: isDuplicate, + onDeleted: () => context.read().add( + RemoveSubSpace(subSpace), + ), + ); + }, + ), + SizedBox( + width: 200, + child: TextField( + controller: _subspaceNameController, + decoration: InputDecoration( + border: InputBorder.none, + hintText: subSpaces.isEmpty ? 'Please enter the name' : null, + hintStyle: context.textTheme.bodySmall?.copyWith( + color: ColorsManager.lightGrayColor, + ), + ), + onSubmitted: (value) { + final trimmedValue = value.trim(); + if (trimmedValue.isNotEmpty) { + context.read().add( + AddSubSpace( + SubspaceModel( + subspaceName: trimmedValue, + disabled: false, + ), + ), + ); + _subspaceNameController.clear(); + } + }, + style: context.textTheme.bodyMedium, + ), + ), + ], + ), + ); + } +} diff --git a/lib/pages/spaces_management/structure_selector/view/center_body_widget.dart b/lib/pages/spaces_management/structure_selector/view/center_body_widget.dart index 0f40ddbb..dbc6c7ef 100644 --- a/lib/pages/spaces_management/structure_selector/view/center_body_widget.dart +++ b/lib/pages/spaces_management/structure_selector/view/center_body_widget.dart @@ -14,11 +14,11 @@ class CenterBodyWidget extends StatelessWidget { if (state is InitialState) { context.read().add(CommunityStructureSelectedEvent()); } - if (state is CommunityStructureState) { + if (state is CommunityStructureState) { context.read().add(BlankStateEvent(context)); } - if (state is SpaceModelState) { + if (state is SpaceModelState) { context.read().add(SpaceModelLoadEvent(context)); } @@ -30,16 +30,23 @@ class CenterBodyWidget extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ GestureDetector( - onTap: () { - context.read().add(CommunityStructureSelectedEvent()); - }, + onTap: state is CommunityStructureState || + state is CommunitySelectedState + ? null + : () { + context + .read() + .add(CommunityStructureSelectedEvent()); + }, child: Text( 'Community Structure', style: Theme.of(context).textTheme.bodyLarge!.copyWith( - fontWeight: state is CommunityStructureState || state is CommunitySelectedState + fontWeight: state is CommunityStructureState || + state is CommunitySelectedState ? FontWeight.bold : FontWeight.normal, - color: state is CommunityStructureState || state is CommunitySelectedState + color: state is CommunityStructureState || + state is CommunitySelectedState ? Theme.of(context).textTheme.bodyLarge!.color : Theme.of(context) .textTheme @@ -50,26 +57,26 @@ class CenterBodyWidget extends StatelessWidget { ), ), const SizedBox(width: 20), - GestureDetector( - onTap: () { - context.read().add(SpaceModelSelectedEvent()); - }, - child: Text( - 'Space Model', - style: Theme.of(context).textTheme.bodyLarge!.copyWith( - fontWeight: state is SpaceModelState - ? FontWeight.bold - : FontWeight.normal, - color: state is SpaceModelState - ? Theme.of(context).textTheme.bodyLarge!.color - : Theme.of(context) - .textTheme - .bodyLarge! - .color! - .withOpacity(0.5), - ), - ), - ), + // GestureDetector( + // onTap: () { + // context.read().add(SpaceModelSelectedEvent()); + // }, + // child: Text( + // 'Space Model', + // style: Theme.of(context).textTheme.bodyLarge!.copyWith( + // fontWeight: state is SpaceModelState + // ? FontWeight.bold + // : FontWeight.normal, + // color: state is SpaceModelState + // ? Theme.of(context).textTheme.bodyLarge!.color + // : Theme.of(context) + // .textTheme + // .bodyLarge! + // .color! + // .withOpacity(0.5), + // ), + // ), + // ), ], ), ], diff --git a/lib/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart b/lib/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart index 9c617a12..a0081c22 100644 --- a/lib/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart +++ b/lib/pages/spaces_management/tag_model/bloc/add_device_model_bloc.dart @@ -24,37 +24,22 @@ class AddDeviceTypeModelBloc if (currentState is AddDeviceModelLoaded) { final existingProduct = currentState.selectedProducts.firstWhere( - (p) => p.productId == event.productId, - orElse: () => SelectedProduct( - productId: event.productId, - count: 0, - productName: event.productName, - product: event.product, - ), + (p) => p.productId == event.selectedProduct.productId, + orElse: () => event.selectedProduct, ); List updatedProducts; - if (event.count > 0) { + if (event.selectedProduct.count > 0) { if (!currentState.selectedProducts.contains(existingProduct)) { updatedProducts = [ ...currentState.selectedProducts, - SelectedProduct( - productId: event.productId, - count: event.count, - productName: event.productName, - product: event.product, - ), + event.selectedProduct, ]; } else { updatedProducts = currentState.selectedProducts.map((p) { - if (p.productId == event.productId) { - return SelectedProduct( - productId: p.productId, - count: event.count, - productName: p.productName, - product: p.product, - ); + if (p.productId == event.selectedProduct.productId) { + return event.selectedProduct; } return p; }).toList(); @@ -62,7 +47,7 @@ class AddDeviceTypeModelBloc } else { // Remove the product if the count is 0 updatedProducts = currentState.selectedProducts - .where((p) => p.productId != event.productId) + .where((p) => p.productId != event.selectedProduct.productId) .toList(); } diff --git a/lib/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart b/lib/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart index b9018b2b..27f183a6 100644 --- a/lib/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart +++ b/lib/pages/spaces_management/tag_model/bloc/add_device_type_model_event.dart @@ -10,20 +10,15 @@ abstract class AddDeviceTypeModelEvent extends Equatable { List get props => []; } - class UpdateProductCountEvent extends AddDeviceTypeModelEvent { - final String productId; - final int count; - final String productName; - final ProductModel product; + final SelectedProduct selectedProduct; - UpdateProductCountEvent({required this.productId, required this.count, required this.productName, required this.product}); + UpdateProductCountEvent({required this.selectedProduct}); @override - List get props => [productId, count]; + List get props => [selectedProduct]; } - class InitializeDeviceTypeModel extends AddDeviceTypeModelEvent { final List initialTags; final List addedProducts; diff --git a/lib/pages/spaces_management/tag_model/widgets/device_type_tile_widget.dart b/lib/pages/spaces_management/tag_model/widgets/device_type_tile_widget.dart index 7d103cdb..3d645d7c 100644 --- a/lib/pages/spaces_management/tag_model/widgets/device_type_tile_widget.dart +++ b/lib/pages/spaces_management/tag_model/widgets/device_type_tile_widget.dart @@ -54,10 +54,10 @@ class DeviceTypeTileWidget extends StatelessWidget { onCountChanged: (newCount) { context.read().add( UpdateProductCountEvent( - productId: product.uuid, - count: newCount, - productName: product.catName, - product: product), + selectedProduct: product.toSelectedProduct( + newCount, + ), + ), ); }, ), diff --git a/lib/pages/visitor_password/view/visitor_password_dialog.dart b/lib/pages/visitor_password/view/visitor_password_dialog.dart index 4db5017c..1e43af46 100644 --- a/lib/pages/visitor_password/view/visitor_password_dialog.dart +++ b/lib/pages/visitor_password/view/visitor_password_dialog.dart @@ -32,13 +32,13 @@ class VisitorPasswordDialog extends StatelessWidget { .stateDialog( context: context, message: 'Password Created Successfully', - title: 'Send Success', + title: 'Sent Successfully', widgeta: Column( children: [ if (visitorBloc.passwordStatus!.failedOperations.isNotEmpty) Column( children: [ - const Text('Failed Devises'), + const Text('Failed Devices'), SizedBox( width: 200, height: 50, @@ -63,7 +63,7 @@ class VisitorPasswordDialog extends StatelessWidget { if (visitorBloc.passwordStatus!.successOperations.isNotEmpty) Column( children: [ - const Text('Success Devises'), + const Text('Success Devices'), SizedBox( width: 200, height: 50, @@ -95,7 +95,7 @@ class VisitorPasswordDialog extends StatelessWidget { visitorBloc.stateDialog( context: context, message: state.message, - title: 'Something Wrong', + title: 'Something went wrong', ); } }, diff --git a/lib/services/api/api_exception.dart b/lib/services/api/api_exception.dart new file mode 100644 index 00000000..89d969d3 --- /dev/null +++ b/lib/services/api/api_exception.dart @@ -0,0 +1,10 @@ +class APIException implements Exception { + final String message; + + APIException(this.message); + + @override + String toString() { + return message; + } +} diff --git a/lib/services/api/http_service.dart b/lib/services/api/http_service.dart index b75f05cf..c76291bf 100644 --- a/lib/services/api/http_service.dart +++ b/lib/services/api/http_service.dart @@ -22,6 +22,18 @@ class HTTPService { ); client.interceptors.add(serviceLocator.get()); + // Add this interceptor for logging requests and responses + // client.interceptors.add( + // LogInterceptor( + // request: true, + // requestHeader: true, + // requestBody: true, + // responseHeader: false, + // responseBody: true, + // error: true, + // logPrint: (object) => print(object), + // ), + // ); return client; } diff --git a/lib/services/auth_api.dart b/lib/services/auth_api.dart index 190eb624..18d951c1 100644 --- a/lib/services/auth_api.dart +++ b/lib/services/auth_api.dart @@ -1,18 +1,26 @@ +import 'package:dio/dio.dart'; import 'package:syncrow_web/pages/auth/model/region_model.dart'; import 'package:syncrow_web/pages/auth/model/token.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/constants/api_const.dart'; class AuthenticationAPI { static Future loginWithEmail({required var model}) async { - final response = await HTTPService().post( - path: ApiEndpoints.login, - body: model.toJson(), - showServerMessage: true, - expectedResponseModel: (json) { - return Token.fromJson(json['data']); - }); - return response; + try { + final response = await HTTPService().post( + path: ApiEndpoints.login, + body: model.toJson(), + showServerMessage: true, + expectedResponseModel: (json) { + return Token.fromJson(json['data']); + }); + return response; + } on DioException catch (e) { + final message = e.response?.data['error']['message'] ?? + 'An error occurred while logging in'; + throw APIException(message); + } } static Future forgetPassword({ @@ -20,12 +28,18 @@ class AuthenticationAPI { required var password, required var otpCode, }) async { - final response = await HTTPService().post( - path: ApiEndpoints.forgetPassword, - body: {"email": email, "password": password, "otpCode": otpCode}, - showServerMessage: true, - expectedResponseModel: (json) {}); - return response; + try { + final response = await HTTPService().post( + path: ApiEndpoints.forgetPassword, + body: {"email": email, "password": password, "otpCode": otpCode}, + showServerMessage: true, + expectedResponseModel: (json) {}); + return response; + } on DioException catch (e) { + final message = e.response?.data['error']['message'] ?? + 'An error occurred while resetting the password'; + throw APIException(message); + } } static Future sendOtp({required String email}) async { @@ -39,19 +53,26 @@ class AuthenticationAPI { return response; } - static Future verifyOtp({required String email, required String otpCode}) async { - final response = await HTTPService().post( - path: ApiEndpoints.verifyOtp, - body: {"email": email, "type": "PASSWORD", "otpCode": otpCode}, - showServerMessage: true, - expectedResponseModel: (json) { - if (json['message'] == 'Otp Verified Successfully') { - return true; - } else { - return false; - } - }); - return response; + static Future verifyOtp( + {required String email, required String otpCode}) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.verifyOtp, + body: {"email": email, "type": "PASSWORD", "otpCode": otpCode}, + showServerMessage: true, + expectedResponseModel: (json) { + if (json['message'] == 'Otp Verified Successfully') { + return true; + } else { + return false; + } + }); + return response; + } on APIException catch (e) { + throw APIException(e.message); + } catch (e) { + throw APIException('An error occurred while verifying the OTP'); + } } static Future> fetchRegion() async { @@ -59,7 +80,9 @@ class AuthenticationAPI { path: ApiEndpoints.getRegion, showServerMessage: true, expectedResponseModel: (json) { - return (json as List).map((zone) => RegionModel.fromJson(zone)).toList(); + return (json as List) + .map((zone) => RegionModel.fromJson(zone)) + .toList(); }); return response; } diff --git a/lib/services/devices_mang_api.dart b/lib/services/devices_mang_api.dart index b4de6326..6f60e34f 100644 --- a/lib/services/devices_mang_api.dart +++ b/lib/services/devices_mang_api.dart @@ -91,7 +91,8 @@ class DevicesManagementApi { } } - Future deviceBatchControl(List uuids, String code, dynamic value) async { + Future deviceBatchControl( + List uuids, String code, dynamic value) async { try { final body = { 'devicesUuid': uuids, @@ -116,7 +117,8 @@ class DevicesManagementApi { } } - static Future> getDevicesByGatewayId(String gatewayId) async { + static Future> getDevicesByGatewayId( + String gatewayId) async { final response = await HTTPService().get( path: ApiEndpoints.gatewayApi.replaceAll('{gatewayUuid}', gatewayId), showServerMessage: false, @@ -150,7 +152,9 @@ class DevicesManagementApi { String code, ) async { final response = await HTTPService().get( - path: ApiEndpoints.getDeviceLogs.replaceAll('{uuid}', uuid).replaceAll('{code}', code), + path: ApiEndpoints.getDeviceLogs + .replaceAll('{uuid}', uuid) + .replaceAll('{code}', code), showServerMessage: false, expectedResponseModel: (json) { return DeviceReport.fromJson(json['data']); @@ -223,7 +227,8 @@ class DevicesManagementApi { } } - Future addScheduleRecord(ScheduleEntry sendSchedule, String uuid) async { + Future addScheduleRecord( + ScheduleEntry sendSchedule, String uuid) async { try { final response = await HTTPService().post( path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), @@ -240,7 +245,8 @@ class DevicesManagementApi { } } - Future> getDeviceSchedules(String uuid, String category) async { + Future> getDeviceSchedules( + String uuid, String category) async { try { final response = await HTTPService().get( path: ApiEndpoints.getScheduleByDeviceId @@ -263,7 +269,9 @@ class DevicesManagementApi { } Future updateScheduleRecord( - {required bool enable, required String uuid, required String scheduleId}) async { + {required bool enable, + required String uuid, + required String scheduleId}) async { try { final response = await HTTPService().put( path: ApiEndpoints.updateScheduleByDeviceId @@ -284,7 +292,8 @@ class DevicesManagementApi { } } - Future editScheduleRecord(String uuid, ScheduleEntry newSchedule) async { + Future editScheduleRecord( + String uuid, ScheduleEntry newSchedule) async { try { final response = await HTTPService().put( path: ApiEndpoints.scheduleByDeviceId.replaceAll('{deviceUuid}', uuid), @@ -335,4 +344,46 @@ class DevicesManagementApi { return false; } } + + static Future> putDeviceName( + {required String deviceId, required String deviceName}) async { + try { + final response = await HTTPService().put( + path: ApiEndpoints.deviceByUuid.replaceAll('{deviceUuid}', deviceId), + body: {"deviceName": deviceName}, + expectedResponseModel: (json) { + return json['data']; + }, + ); + return response; + } catch (e) { + rethrow; + } + } + + static Future getDeviceInfo(String deviceId) async { + final response = await HTTPService().get( + path: ApiEndpoints.deviceByUuid.replaceAll('{deviceUuid}', deviceId), + showServerMessage: false, + expectedResponseModel: (json) { + return json['data'] as Map; + }); + return response; + } + + static Future resetDevice({ + String? devicesUuid, + }) async { + final response = await HTTPService().post( + path: ApiEndpoints.resetDevice.replaceAll('{deviceUuid}', devicesUuid!), + showServerMessage: false, + body: { + "devicesUuid": [devicesUuid] + }, + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } } diff --git a/lib/services/routines_api.dart b/lib/services/routines_api.dart index eaa09e27..bdc46ac1 100644 --- a/lib/services/routines_api.dart +++ b/lib/services/routines_api.dart @@ -1,3 +1,4 @@ +import 'package:dio/dio.dart'; import 'package:flutter/material.dart'; import 'package:syncrow_web/pages/routines/bloc/automation_scene_trigger_bloc/automation_status_update.dart'; import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/create_automation_model.dart'; @@ -5,6 +6,7 @@ import 'package:syncrow_web/pages/routines/models/create_scene_and_autoamtion/cr import 'package:syncrow_web/pages/routines/models/icon_model.dart'; import 'package:syncrow_web/pages/routines/models/routine_details_model.dart'; import 'package:syncrow_web/pages/routines/models/routine_model.dart'; +import 'package:syncrow_web/services/api/api_exception.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/constants/api_const.dart'; @@ -26,9 +28,10 @@ class SceneApi { ); debugPrint('create scene response: $response'); return response; - } catch (e) { - debugPrint(e.toString()); - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -48,9 +51,10 @@ class SceneApi { ); debugPrint('create automation response: $response'); return response; - } catch (e) { - debugPrint(e.toString()); - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -165,8 +169,10 @@ class SceneApi { }, ); return response; - } catch (e) { - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -185,8 +191,10 @@ class SceneApi { }, ); return response; - } catch (e) { - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -217,8 +225,10 @@ class SceneApi { expectedResponseModel: (json) => json['statusCode'] == 200, ); return response; - } catch (e) { - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } @@ -236,8 +246,10 @@ class SceneApi { expectedResponseModel: (json) => json['statusCode'] == 200, ); return response; - } catch (e) { - rethrow; + } on DioException catch (e) { + String errorMessage = + e.response?.data['error']['message'][0] ?? 'something went wrong'; + throw APIException(errorMessage); } } diff --git a/lib/services/space_mana_api.dart b/lib/services/space_mana_api.dart index 19e219b6..14902bca 100644 --- a/lib/services/space_mana_api.dart +++ b/lib/services/space_mana_api.dart @@ -1,25 +1,31 @@ import 'package:flutter/material.dart'; +import 'package:syncrow_web/pages/device_managment/device_setting/settings_model/sub_space_model.dart'; import 'package:syncrow_web/pages/space_tree/model/pagination_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/community_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/create_subspace_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_model.dart'; import 'package:syncrow_web/pages/spaces_management/all_spaces/model/space_response_model.dart'; +import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_body_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/tag_update_model.dart'; import 'package:syncrow_web/services/api/http_service.dart'; import 'package:syncrow_web/utils/constants/api_const.dart'; +import '../pages/spaces_management/all_spaces/model/subspace_model.dart'; + class CommunitySpaceManagementApi { // Community Management APIs - Future> fetchCommunities(String projectId, {int page = 1}) async { + Future> fetchCommunities(String projectId, + {int page = 1}) async { try { List allCommunities = []; bool hasNext = true; while (hasNext) { await HTTPService().get( - path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), + path: ApiEndpoints.getCommunityList + .replaceAll('{projectId}', projectId), queryParameters: { 'page': page, }, @@ -55,8 +61,14 @@ class CommunitySpaceManagementApi { try { bool hasNext = false; await HTTPService().get( - path: ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), - queryParameters: {'page': page, 'includeSpaces': true, 'size': 25, 'search': search}, + path: + ApiEndpoints.getCommunityList.replaceAll('{projectId}', projectId), + queryParameters: { + 'page': page, + 'includeSpaces': true, + 'size': 25, + 'search': search + }, expectedResponseModel: (json) { try { List jsonData = json['data'] ?? []; @@ -68,7 +80,10 @@ class CommunitySpaceManagementApi { page = currentPage + 1; paginationModel = PaginationModel( - pageNum: page, hasNext: hasNext, size: 25, communities: communityList); + pageNum: page, + hasNext: hasNext, + size: 25, + communities: communityList); return paginationModel; } catch (_) { hasNext = false; @@ -83,7 +98,8 @@ class CommunitySpaceManagementApi { Future getCommunityById(String communityId) async { try { final response = await HTTPService().get( - path: ApiEndpoints.getCommunityById.replaceAll('{communityId}', communityId), + path: ApiEndpoints.getCommunityById + .replaceAll('{communityId}', communityId), expectedResponseModel: (json) { return CommunityModel.fromJson(json['data']); }, @@ -95,7 +111,8 @@ class CommunitySpaceManagementApi { } } - Future createCommunity(String name, String description, String projectId) async { + Future createCommunity( + String name, String description, String projectId) async { try { final response = await HTTPService().post( path: ApiEndpoints.createCommunity.replaceAll('{projectId}', projectId), @@ -114,7 +131,8 @@ class CommunitySpaceManagementApi { } } - Future updateCommunity(String communityId, String name, String projectId) async { + Future updateCommunity( + String communityId, String name, String projectId) async { try { final response = await HTTPService().put( path: ApiEndpoints.updateCommunity @@ -151,7 +169,8 @@ class CommunitySpaceManagementApi { } } - Future fetchSpaces(String communityId, String projectId) async { + Future fetchSpaces( + String communityId, String projectId) async { try { final response = await HTTPService().get( path: ApiEndpoints.listSpaces @@ -177,7 +196,8 @@ class CommunitySpaceManagementApi { } } - Future getSpace(String communityId, String spaceId, String projectId) async { + Future getSpace( + String communityId, String spaceId, String projectId) async { try { final response = await HTTPService().get( path: ApiEndpoints.getSpace @@ -199,7 +219,6 @@ class CommunitySpaceManagementApi { {required String communityId, required String name, String? parentId, - String? direction, bool isPrivate = false, required Offset position, String? spaceModelUuid, @@ -213,7 +232,6 @@ class CommunitySpaceManagementApi { 'isPrivate': isPrivate, 'x': position.dx, 'y': position.dy, - 'direction': direction, 'icon': icon, }; if (parentId != null) { @@ -248,11 +266,10 @@ class CommunitySpaceManagementApi { required String name, String? parentId, String? icon, - String? direction, bool isPrivate = false, required Offset position, - List? tags, - List? subspaces, + List? tags, + List? subspaces, String? spaceModelUuid, required String projectId}) async { try { @@ -261,9 +278,8 @@ class CommunitySpaceManagementApi { 'isPrivate': isPrivate, 'x': position.dx, 'y': position.dy, - 'direction': direction, 'icon': icon, - 'subspace': subspaces, + 'subspaces': subspaces, 'tags': tags, 'spaceModelUuid': spaceModelUuid, }; @@ -289,7 +305,8 @@ class CommunitySpaceManagementApi { } } - Future deleteSpace(String communityId, String spaceId, String projectId) async { + Future deleteSpace( + String communityId, String spaceId, String projectId) async { try { final response = await HTTPService().delete( path: ApiEndpoints.deleteSpace @@ -307,15 +324,17 @@ class CommunitySpaceManagementApi { } } - Future> getSpaceHierarchy(String communityId, String projectId) async { + Future> getSpaceHierarchy( + String communityId, String projectId) async { try { final response = await HTTPService().get( path: ApiEndpoints.getSpaceHierarchy .replaceAll('{communityId}', communityId) .replaceAll('{projectId}', projectId), expectedResponseModel: (json) { - final spaceModels = - (json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList(); + final spaceModels = (json['data'] as List) + .map((spaceJson) => SpaceModel.fromJson(spaceJson)) + .toList(); return spaceModels; }, @@ -327,15 +346,17 @@ class CommunitySpaceManagementApi { } } - Future> getSpaceOnlyWithDevices({String? communityId, String? projectId}) async { + Future> getSpaceOnlyWithDevices( + {String? communityId, String? projectId}) async { try { final response = await HTTPService().get( path: ApiEndpoints.spaceOnlyWithDevices .replaceAll('{communityId}', communityId!) .replaceAll('{projectId}', projectId!), expectedResponseModel: (json) { - final spaceModels = - (json['data'] as List).map((spaceJson) => SpaceModel.fromJson(spaceJson)).toList(); + final spaceModels = (json['data'] as List) + .map((spaceJson) => SpaceModel.fromJson(spaceJson)) + .toList(); return spaceModels; }, ); @@ -345,4 +366,59 @@ class CommunitySpaceManagementApi { return []; } } + + static Future> getSubSpaceBySpaceId( + {required String communityId, + required String spaceId, + required String projectId}) async { + try { + final path = ApiEndpoints.listSubspace + .replaceFirst('{communityUuid}', communityId) + .replaceFirst('{spaceUuid}', spaceId) + .replaceAll('{projectUuid}', projectId); + + final response = await HTTPService().get( + path: path, + queryParameters: {"page": 1, "pageSize": 10}, + showServerMessage: false, + expectedResponseModel: (json) { + List rooms = []; + if (json['data'] != null) { + for (var subspace in json['data']) { + rooms.add(SubSpaceModel.fromJson(subspace)); + } + } + return rooms; + }, + ); + + return response; + } catch (error, stackTrace) { + return []; + } + } + + static Future> assignDeviceToRoom( + {required String communityId, + required String spaceId, + required String subSpaceId, + required String deviceId, + required String projectId}) async { + try { + final response = await HTTPService().post( + path: ApiEndpoints.assignDeviceToRoom + .replaceAll('{projectUuid}', projectId) + .replaceAll('{communityUuid}', communityId) + .replaceAll('{spaceUuid}', spaceId) + .replaceAll('{subSpaceUuid}', subSpaceId) + .replaceAll('{deviceUuid}', deviceId), + expectedResponseModel: (json) { + return json; + }, + ); + return response; + } catch (e) { + rethrow; + } + } } diff --git a/lib/services/space_model_mang_api.dart b/lib/services/space_model_mang_api.dart index 5ae3e4d9..cbb9cfeb 100644 --- a/lib/services/space_model_mang_api.dart +++ b/lib/services/space_model_mang_api.dart @@ -1,3 +1,5 @@ +import 'dart:developer'; + import 'package:syncrow_web/pages/spaces_management/all_spaces/model/tag.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/create_space_template_body_model.dart'; import 'package:syncrow_web/pages/spaces_management/space_model/models/space_template_model.dart'; @@ -7,17 +9,23 @@ import 'package:syncrow_web/utils/constants/api_const.dart'; class SpaceModelManagementApi { Future> listSpaceModels( {required String projectId, int page = 1}) async { - final response = await HTTPService().get( - path: ApiEndpoints.listSpaceModels.replaceAll('{projectId}', projectId), - queryParameters: {'page': page}, - expectedResponseModel: (json) { - List jsonData = json['data']; - return jsonData.map((jsonItem) { - return SpaceTemplateModel.fromJson(jsonItem); - }).toList(); - }, - ); - return response; + try { + // final response = await HTTPService().get( + // path: ApiEndpoints.listSpaceModels.replaceAll('{projectId}', projectId), + // queryParameters: {'page': page}, + // expectedResponseModel: (json) { + // List jsonData = json['data']; + // return jsonData.map((jsonItem) { + // return SpaceTemplateModel.fromJson(jsonItem); + // }).toList(); + // }, + // ); + return []; + // response; + } catch (e) { + log(e.toString()); + return []; + } } Future createSpaceModel( @@ -33,8 +41,8 @@ class SpaceModelManagementApi { return response; } - Future updateSpaceModel( - CreateSpaceTemplateBodyModel spaceModel, String spaceModelUuid, String projectId) async { + Future updateSpaceModel(CreateSpaceTemplateBodyModel spaceModel, + String spaceModelUuid, String projectId) async { final response = await HTTPService().put( path: ApiEndpoints.updateSpaceModel .replaceAll('{projectId}', projectId) @@ -47,7 +55,8 @@ class SpaceModelManagementApi { return response; } - Future getSpaceModel(String spaceModelUuid, String projectId) async { + Future getSpaceModel( + String spaceModelUuid, String projectId) async { final response = await HTTPService().get( path: ApiEndpoints.getSpaceModel .replaceAll('{projectId}', projectId) diff --git a/lib/utils/color_manager.dart b/lib/utils/color_manager.dart index 5a892aa6..50170ed9 100644 --- a/lib/utils/color_manager.dart +++ b/lib/utils/color_manager.dart @@ -73,4 +73,17 @@ abstract class ColorsManager { static const Color vividBlue = Color(0xFF023DFE); static const Color semiTransparentRed = Color(0x99FF0000); static const Color grey700 = Color(0xFF2D3748); + static const Color goodGreen = Color(0xFF0CEC16); + static const Color moderateYellow = Color(0xFFFAC96C); + static const Color poorOrange = Color(0xFFEC7400); + static const Color unhealthyRed = Color(0xFFD40000); + static const Color severePink = Color(0xFFD40094); + static const Color hazardousPurple = Color(0xFFBA01FD); + static const Color maxPurple = Color(0xFF962DFF); + static const Color maxPurpleDot = Color(0xFF5F00BD); + static const Color minBlue = Color(0xFF93AAFD); + static const Color minBlueDot = Color(0xFF023DFE); + static const Color grey25 = Color(0xFFF9F9F9); + + } diff --git a/lib/utils/constants/api_const.dart b/lib/utils/constants/api_const.dart index 8de9efcf..d58d0f28 100644 --- a/lib/utils/constants/api_const.dart +++ b/lib/utils/constants/api_const.dart @@ -60,9 +60,12 @@ abstract class ApiEndpoints { '/devices/{uuid}/report-logs?code={code}&startTime={startTime}&endTime={endTime}'; static const String scheduleByDeviceId = '/schedule/{deviceUuid}'; - static const String getScheduleByDeviceId = '/schedule/{deviceUuid}?category={category}'; - static const String deleteScheduleByDeviceId = '/schedule/{deviceUuid}/{scheduleUuid}'; - static const String updateScheduleByDeviceId = '/schedule/enable/{deviceUuid}'; + static const String getScheduleByDeviceId = + '/schedule/{deviceUuid}?category={category}'; + static const String deleteScheduleByDeviceId = + '/schedule/{deviceUuid}/{scheduleUuid}'; + static const String updateScheduleByDeviceId = + '/schedule/enable/{deviceUuid}'; static const String factoryReset = '/devices/batch'; //product @@ -124,4 +127,13 @@ abstract class ApiEndpoints { '/projects/{projectId}/communities/{communityId}/spaces/{unitUuid}/automations'; static const String spaceOnlyWithDevices = '/projects/{projectId}/communities/{communityId}/spaces?onlyWithDevices=true'; + + static const String listSubspace = + '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces'; + static const String deviceByUuid = '/devices/{deviceUuid}'; + + static const String resetDevice = '/factory/reset/{deviceUuid}'; + + static const String assignDeviceToRoom = + '/projects/{projectUuid}/communities/{communityUuid}/spaces/{spaceUuid}/subspaces/{subSpaceUuid}/devices/{deviceUuid}'; } diff --git a/lib/utils/constants/assets.dart b/lib/utils/constants/assets.dart index f857a357..dfc0b394 100644 --- a/lib/utils/constants/assets.dart +++ b/lib/utils/constants/assets.dart @@ -2,6 +2,7 @@ class Assets { Assets._(); static const String background = "assets/images/Background.png"; static const String webBackground = "assets/images/web_Background.svg"; + static const String webBackgroundPng = "assets/images/web_Background.png"; static const String blackLogo = "assets/images/black-logo.png"; static const String logo = "assets/images/Logo.svg"; static const String logoHorizontal = "assets/images/logo_horizontal.png"; @@ -20,6 +21,8 @@ class Assets { static const String spaseManagementIcon = "assets/images/spase_management_icon.svg"; static const String devicesIcon = "assets/images/devices_icon.svg"; + static const String analyticsIcon = "assets/icons/landing_analytics.svg"; + static const String moveinIcon = "assets/images/movein_icon.svg"; static const String constructionIcon = "assets/images/construction_icon.svg"; static const String energyIcon = "assets/images/energy_icon.svg"; @@ -481,4 +484,21 @@ class Assets { static const String indentLevelIcon = 'assets/icons/indent_level_icon.svg'; static const String triggerLevelIcon = 'assets/icons/trigger_level_icon.svg'; static const String blankCalendar = 'assets/icons/blank_calendar.svg'; + static const String refreshStatusIcon = + 'assets/icons/refresh_status_icon.svg'; + static const String energyConsumedIcon = + 'assets/icons/energy_consumed_icon.svg'; + + static const String closeSettingsIcon = + 'assets/icons/close_settings_icon.svg'; + + static const String editNameIconSettings = + 'assets/icons/edit_name_icon_settings.svg'; + + static const String locationPin = 'assets/icons/location_pin.svg'; + static const String aqiTemperature = 'assets/icons/aqi_temperature.svg'; + static const String aqiHumidity = 'assets/icons/aqi_humidity.svg'; + static const String aqiAirQuality = 'assets/icons/aqi_air_quality.svg'; + static const String temperatureAqiSidebar = 'assets/icons/thermometer.svg'; + static const String humidityAqiSidebar = 'assets/icons/humidity.svg'; } diff --git a/lib/utils/enum/device_types.dart b/lib/utils/enum/device_types.dart index 7ad8e02c..9bfd322f 100644 --- a/lib/utils/enum/device_types.dart +++ b/lib/utils/enum/device_types.dart @@ -19,6 +19,7 @@ enum DeviceType { WaterLeak, NCPS, DoorSensor, + PC, Other, } /* @@ -59,4 +60,5 @@ Map devicesTypesMap = { 'GD': DeviceType.GarageDoor, 'WL': DeviceType.WaterLeak, 'NCPS': DeviceType.NCPS, + 'PC': DeviceType.PC, }; diff --git a/lib/utils/user_drop_down_menu.dart b/lib/utils/user_drop_down_menu.dart index c9fe492e..15da1f3a 100644 --- a/lib/utils/user_drop_down_menu.dart +++ b/lib/utils/user_drop_down_menu.dart @@ -41,13 +41,24 @@ class _UserDropdownMenuState extends State { _isDropdownOpen = false; }); }, - child: Transform.rotate( - angle: _isDropdownOpen ? -1.5708 : 1.5708, - child: const Icon( - Icons.arrow_forward_ios, - color: Colors.white, - size: 16, - ), + child: Row( + children: [ + const SizedBox(width: 12), + if (widget.user != null) + Text( + '${widget.user!.firstName} ${widget.user!.lastName}', + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(width: 12), + Transform.rotate( + angle: _isDropdownOpen ? -1.5708 : 1.5708, + child: const Icon( + Icons.arrow_forward_ios, + color: Colors.white, + size: 16, + ), + ), + ], ), ), ], diff --git a/lib/web_layout/default_container.dart b/lib/web_layout/default_container.dart new file mode 100644 index 00000000..e0a71b04 --- /dev/null +++ b/lib/web_layout/default_container.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; + +class DefaultContainer extends StatelessWidget { + const DefaultContainer({ + super.key, + required this.child, + this.height, + this.width, + this.color, + this.boxConstraints, + this.margin, + this.padding, + this.onTap, + this.borderRadius, + }); + + final double? height; + final double? width; + final Widget child; + final BoxConstraints? boxConstraints; + final EdgeInsets? margin; + final EdgeInsets? padding; + final Color? color; + final Function()? onTap; + final BorderRadius? borderRadius; + @override + Widget build(BuildContext context) { + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(20), + child: Container( + height: height, + width: width, + margin: margin ?? const EdgeInsets.only(right: 3, bottom: 3), + constraints: boxConstraints, + decoration: BoxDecoration( + color: color ?? Colors.white, + borderRadius: borderRadius ?? BorderRadius.circular(20), + ), + padding: padding ?? const EdgeInsets.all(10), + child: child, + ), + ); + } +} diff --git a/lib/web_layout/web_app_bar.dart b/lib/web_layout/web_app_bar.dart index 02b81522..3cfe171e 100644 --- a/lib/web_layout/web_app_bar.dart +++ b/lib/web_layout/web_app_bar.dart @@ -92,13 +92,6 @@ class DesktopAppBar extends StatelessWidget { if (rightBody != null) rightBody!, const SizedBox(width: 24), _UserAvatar(), - const SizedBox(width: 12), - if (user != null) - Text( - '${user.firstName} ${user.lastName}', - style: Theme.of(context).textTheme.bodyLarge, - ), - const SizedBox(width: 12), UserDropdownMenu(user: user), ], ); @@ -146,14 +139,6 @@ class TabletAppBar extends StatelessWidget { if (rightBody != null) rightBody!, const SizedBox(width: 16), _UserAvatar(), - if (user != null) ...[ - const SizedBox(width: 8), - Text( - '${user.firstName} ${user.lastName}', - style: - Theme.of(context).textTheme.bodyLarge?.copyWith(fontSize: 14), - ), - ], UserDropdownMenu(user: user), ], ); @@ -215,14 +200,6 @@ class MobileAppBar extends StatelessWidget { return Row( children: [ _UserAvatar(), - if (user != null) ...[ - const SizedBox(width: 8), - Text( - '${user.firstName} ${user.lastName}', - style: - Theme.of(context).textTheme.bodyLarge?.copyWith(fontSize: 14), - ), - ], UserDropdownMenu(user: user), ], ); diff --git a/lib/web_layout/web_scaffold.dart b/lib/web_layout/web_scaffold.dart index c1d6075f..a37727db 100644 --- a/lib/web_layout/web_scaffold.dart +++ b/lib/web_layout/web_scaffold.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:syncrow_web/utils/constants/assets.dart'; import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_layout_helper.dart'; import 'package:syncrow_web/web_layout/web_app_bar.dart'; + import 'menu_sidebar.dart'; class WebScaffold extends StatelessWidget with HelperResponsiveLayout { @@ -28,14 +28,11 @@ class WebScaffold extends StatelessWidget with HelperResponsiveLayout { SizedBox( width: MediaQuery.sizeOf(context).width, height: MediaQuery.sizeOf(context).height, - child: SvgPicture.asset( - Assets.webBackground, + child: Image.asset( + Assets.webBackgroundPng, fit: BoxFit.cover, ), ), - Container( - color: Colors.white.withOpacity(0.7), - ), Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ diff --git a/linux/.gitignore b/linux/.gitignore deleted file mode 100644 index d3896c98..00000000 --- a/linux/.gitignore +++ /dev/null @@ -1 +0,0 @@ -flutter/ephemeral diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt deleted file mode 100644 index 3f1132f0..00000000 --- a/linux/CMakeLists.txt +++ /dev/null @@ -1,145 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.10) -project(runner LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "syncrow_web") -# The unique GTK application identifier for this application. See: -# https://wiki.gnome.org/HowDoI/ChooseApplicationID -set(APPLICATION_ID "com.example.syncrow_web") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(SET CMP0063 NEW) - -# Load bundled libraries from the lib/ directory relative to the binary. -set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") - -# Root filesystem for cross-building. -if(FLUTTER_TARGET_PLATFORM_SYSROOT) - set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) - set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) - set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) - set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) -endif() - -# Define build configuration options. -if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") -endif() - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_14) - target_compile_options(${TARGET} PRIVATE -Wall -Werror) - target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") - target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) - -add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") - -# Define the application target. To change its name, change BINARY_NAME above, -# not the value here, or `flutter run` will no longer work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} - "main.cc" - "my_application.cc" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add dependency libraries. Add any application-specific dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter) -target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) - -# Only the install-generated bundle's copy of the executable will launch -# correctly, since the resources must in the right relative locations. To avoid -# people trying to run the unbundled copy, put it in a subdirectory instead of -# the default top-level location. -set_target_properties(${BINARY_NAME} - PROPERTIES - RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" -) - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# By default, "installing" just makes a relocatable bundle in the build -# directory. -set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -# Start with a clean build bundle directory every time. -install(CODE " - file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") - " COMPONENT Runtime) - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) - install(FILES "${bundled_library}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endforeach(bundled_library) - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") - install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() diff --git a/linux/flutter/CMakeLists.txt b/linux/flutter/CMakeLists.txt deleted file mode 100644 index d5bd0164..00000000 --- a/linux/flutter/CMakeLists.txt +++ /dev/null @@ -1,88 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.10) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. - -# Serves the same purpose as list(TRANSFORM ... PREPEND ...), -# which isn't available in 3.10. -function(list_prepend LIST_NAME PREFIX) - set(NEW_LIST "") - foreach(element ${${LIST_NAME}}) - list(APPEND NEW_LIST "${PREFIX}${element}") - endforeach(element) - set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) -endfunction() - -# === Flutter Library === -# System-level dependencies. -find_package(PkgConfig REQUIRED) -pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) -pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) -pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) - -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "fl_basic_message_channel.h" - "fl_binary_codec.h" - "fl_binary_messenger.h" - "fl_dart_project.h" - "fl_engine.h" - "fl_json_message_codec.h" - "fl_json_method_codec.h" - "fl_message_codec.h" - "fl_method_call.h" - "fl_method_channel.h" - "fl_method_codec.h" - "fl_method_response.h" - "fl_plugin_registrar.h" - "fl_plugin_registry.h" - "fl_standard_message_codec.h" - "fl_standard_method_codec.h" - "fl_string_codec.h" - "fl_value.h" - "fl_view.h" - "flutter_linux.h" -) -list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") -target_link_libraries(flutter INTERFACE - PkgConfig::GTK - PkgConfig::GLIB - PkgConfig::GIO -) -add_dependencies(flutter flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CMAKE_CURRENT_BINARY_DIR}/_phony_ - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" - ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} -) diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 38dd0bc6..00000000 --- a/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,19 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include - -void fl_register_plugins(FlPluginRegistry* registry) { - g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); - flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); - g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = - fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); - url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); -} diff --git a/linux/flutter/generated_plugin_registrant.h b/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47b..00000000 --- a/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake deleted file mode 100644 index 65240e99..00000000 --- a/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,25 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - flutter_secure_storage_linux - url_launcher_linux -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/linux/main.cc b/linux/main.cc deleted file mode 100644 index e7c5c543..00000000 --- a/linux/main.cc +++ /dev/null @@ -1,6 +0,0 @@ -#include "my_application.h" - -int main(int argc, char** argv) { - g_autoptr(MyApplication) app = my_application_new(); - return g_application_run(G_APPLICATION(app), argc, argv); -} diff --git a/linux/my_application.cc b/linux/my_application.cc deleted file mode 100644 index 82e018ea..00000000 --- a/linux/my_application.cc +++ /dev/null @@ -1,124 +0,0 @@ -#include "my_application.h" - -#include -#ifdef GDK_WINDOWING_X11 -#include -#endif - -#include "flutter/generated_plugin_registrant.h" - -struct _MyApplication { - GtkApplication parent_instance; - char** dart_entrypoint_arguments; -}; - -G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) - -// Implements GApplication::activate. -static void my_application_activate(GApplication* application) { - MyApplication* self = MY_APPLICATION(application); - GtkWindow* window = - GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); - - // Use a header bar when running in GNOME as this is the common style used - // by applications and is the setup most users will be using (e.g. Ubuntu - // desktop). - // If running on X and not using GNOME then just use a traditional title bar - // in case the window manager does more exotic layout, e.g. tiling. - // If running on Wayland assume the header bar will work (may need changing - // if future cases occur). - gboolean use_header_bar = TRUE; -#ifdef GDK_WINDOWING_X11 - GdkScreen* screen = gtk_window_get_screen(window); - if (GDK_IS_X11_SCREEN(screen)) { - const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); - if (g_strcmp0(wm_name, "GNOME Shell") != 0) { - use_header_bar = FALSE; - } - } -#endif - if (use_header_bar) { - GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); - gtk_widget_show(GTK_WIDGET(header_bar)); - gtk_header_bar_set_title(header_bar, "syncrow_web"); - gtk_header_bar_set_show_close_button(header_bar, TRUE); - gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); - } else { - gtk_window_set_title(window, "syncrow_web"); - } - - gtk_window_set_default_size(window, 1280, 720); - gtk_widget_show(GTK_WIDGET(window)); - - g_autoptr(FlDartProject) project = fl_dart_project_new(); - fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); - - FlView* view = fl_view_new(project); - gtk_widget_show(GTK_WIDGET(view)); - gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); - - fl_register_plugins(FL_PLUGIN_REGISTRY(view)); - - gtk_widget_grab_focus(GTK_WIDGET(view)); -} - -// Implements GApplication::local_command_line. -static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { - MyApplication* self = MY_APPLICATION(application); - // Strip out the first argument as it is the binary name. - self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); - - g_autoptr(GError) error = nullptr; - if (!g_application_register(application, nullptr, &error)) { - g_warning("Failed to register: %s", error->message); - *exit_status = 1; - return TRUE; - } - - g_application_activate(application); - *exit_status = 0; - - return TRUE; -} - -// Implements GApplication::startup. -static void my_application_startup(GApplication* application) { - //MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application startup. - - G_APPLICATION_CLASS(my_application_parent_class)->startup(application); -} - -// Implements GApplication::shutdown. -static void my_application_shutdown(GApplication* application) { - //MyApplication* self = MY_APPLICATION(object); - - // Perform any actions required at application shutdown. - - G_APPLICATION_CLASS(my_application_parent_class)->shutdown(application); -} - -// Implements GObject::dispose. -static void my_application_dispose(GObject* object) { - MyApplication* self = MY_APPLICATION(object); - g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); - G_OBJECT_CLASS(my_application_parent_class)->dispose(object); -} - -static void my_application_class_init(MyApplicationClass* klass) { - G_APPLICATION_CLASS(klass)->activate = my_application_activate; - G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; - G_APPLICATION_CLASS(klass)->startup = my_application_startup; - G_APPLICATION_CLASS(klass)->shutdown = my_application_shutdown; - G_OBJECT_CLASS(klass)->dispose = my_application_dispose; -} - -static void my_application_init(MyApplication* self) {} - -MyApplication* my_application_new() { - return MY_APPLICATION(g_object_new(my_application_get_type(), - "application-id", APPLICATION_ID, - "flags", G_APPLICATION_NON_UNIQUE, - nullptr)); -} diff --git a/linux/my_application.h b/linux/my_application.h deleted file mode 100644 index 72271d5e..00000000 --- a/linux/my_application.h +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef FLUTTER_MY_APPLICATION_H_ -#define FLUTTER_MY_APPLICATION_H_ - -#include - -G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, - GtkApplication) - -/** - * my_application_new: - * - * Creates a new Flutter-based application. - * - * Returns: a new #MyApplication. - */ -MyApplication* my_application_new(); - -#endif // FLUTTER_MY_APPLICATION_H_ diff --git a/macos/.gitignore b/macos/.gitignore deleted file mode 100644 index 746adbb6..00000000 --- a/macos/.gitignore +++ /dev/null @@ -1,7 +0,0 @@ -# Flutter-related -**/Flutter/ephemeral/ -**/Pods/ - -# Xcode-related -**/dgph -**/xcuserdata/ diff --git a/macos/DerivedData/Runner/Logs/Build/.dat.nosync1585.W9c579 b/macos/DerivedData/Runner/Logs/Build/.dat.nosync1585.W9c579 deleted file mode 100644 index 309af193..00000000 --- a/macos/DerivedData/Runner/Logs/Build/.dat.nosync1585.W9c579 +++ /dev/null @@ -1,95 +0,0 @@ - - - - - logFormatVersion - 11 - logs - - DEC061F9-9521-4D0C-959C-43A07F62CC12 - - className - IDECommandLineBuildLog - documentTypeString - <nil> - domainType - Xcode.IDEActivityLogDomainType.BuildLog - fileName - DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog - hasPrimaryLog - - primaryObservable - - highLevelStatus - S - totalNumberOfAnalyzerIssues - 0 - totalNumberOfErrors - 0 - totalNumberOfTestFailures - 0 - totalNumberOfWarnings - 0 - - schemeIdentifier-containerName - Runner project - schemeIdentifier-schemeName - Flutter Assemble - schemeIdentifier-sharedScheme - 1 - signature - Cleaning workspace Runner with scheme Flutter Assemble - timeStartedRecording - 752000674.27645695 - timeStoppedRecording - 752000674.42918503 - title - Cleaning workspace Runner with scheme Flutter Assemble - uniqueIdentifier - DEC061F9-9521-4D0C-959C-43A07F62CC12 - - FB42CDDD-C79D-4D4B-891A-12C476DFCB10 - - className - IDECommandLineBuildLog - documentTypeString - <nil> - domainType - Xcode.IDEActivityLogDomainType.BuildLog - fileName - FB42CDDD-C79D-4D4B-891A-12C476DFCB10.xcactivitylog - hasPrimaryLog - - primaryObservable - - highLevelStatus - S - totalNumberOfAnalyzerIssues - 0 - totalNumberOfErrors - 0 - totalNumberOfTestFailures - 0 - totalNumberOfWarnings - 0 - - schemeIdentifier-containerName - Runner project - schemeIdentifier-schemeName - Runner - schemeIdentifier-sharedScheme - 1 - signature - Cleaning workspace Runner with scheme Runner - timeStartedRecording - 752000674.90370798 - timeStoppedRecording - 752000675.05962098 - title - Cleaning workspace Runner with scheme Runner - uniqueIdentifier - FB42CDDD-C79D-4D4B-891A-12C476DFCB10 - - - - diff --git a/macos/DerivedData/Runner/Logs/Build/DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog b/macos/DerivedData/Runner/Logs/Build/DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog deleted file mode 100644 index c811e6cb..00000000 Binary files a/macos/DerivedData/Runner/Logs/Build/DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog and /dev/null differ diff --git a/macos/DerivedData/Runner/Logs/Build/FB42CDDD-C79D-4D4B-891A-12C476DFCB10.xcactivitylog b/macos/DerivedData/Runner/Logs/Build/FB42CDDD-C79D-4D4B-891A-12C476DFCB10.xcactivitylog deleted file mode 100644 index b9ccd504..00000000 Binary files a/macos/DerivedData/Runner/Logs/Build/FB42CDDD-C79D-4D4B-891A-12C476DFCB10.xcactivitylog and /dev/null differ diff --git a/macos/DerivedData/Runner/Logs/Build/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Build/LogStoreManifest.plist deleted file mode 100644 index 0c8e2d35..00000000 --- a/macos/DerivedData/Runner/Logs/Build/LogStoreManifest.plist +++ /dev/null @@ -1,53 +0,0 @@ - - - - - logFormatVersion - 11 - logs - - DEC061F9-9521-4D0C-959C-43A07F62CC12 - - className - IDECommandLineBuildLog - documentTypeString - <nil> - domainType - Xcode.IDEActivityLogDomainType.BuildLog - fileName - DEC061F9-9521-4D0C-959C-43A07F62CC12.xcactivitylog - hasPrimaryLog - - primaryObservable - - highLevelStatus - S - totalNumberOfAnalyzerIssues - 0 - totalNumberOfErrors - 0 - totalNumberOfTestFailures - 0 - totalNumberOfWarnings - 0 - - schemeIdentifier-containerName - Runner project - schemeIdentifier-schemeName - Flutter Assemble - schemeIdentifier-sharedScheme - 1 - signature - Cleaning workspace Runner with scheme Flutter Assemble - timeStartedRecording - 752000674.27645695 - timeStoppedRecording - 752000674.42918503 - title - Cleaning workspace Runner with scheme Flutter Assemble - uniqueIdentifier - DEC061F9-9521-4D0C-959C-43A07F62CC12 - - - - diff --git a/macos/DerivedData/Runner/Logs/Launch/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Launch/LogStoreManifest.plist deleted file mode 100644 index f38de442..00000000 --- a/macos/DerivedData/Runner/Logs/Launch/LogStoreManifest.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - - logFormatVersion - 11 - logs - - - diff --git a/macos/DerivedData/Runner/Logs/Localization/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Localization/LogStoreManifest.plist deleted file mode 100644 index f38de442..00000000 --- a/macos/DerivedData/Runner/Logs/Localization/LogStoreManifest.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - - logFormatVersion - 11 - logs - - - diff --git a/macos/DerivedData/Runner/Logs/Package/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Package/LogStoreManifest.plist deleted file mode 100644 index f38de442..00000000 --- a/macos/DerivedData/Runner/Logs/Package/LogStoreManifest.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - - logFormatVersion - 11 - logs - - - diff --git a/macos/DerivedData/Runner/Logs/Test/LogStoreManifest.plist b/macos/DerivedData/Runner/Logs/Test/LogStoreManifest.plist deleted file mode 100644 index f38de442..00000000 --- a/macos/DerivedData/Runner/Logs/Test/LogStoreManifest.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - - logFormatVersion - 11 - logs - - - diff --git a/macos/DerivedData/Runner/info.plist b/macos/DerivedData/Runner/info.plist deleted file mode 100644 index 3594c152..00000000 --- a/macos/DerivedData/Runner/info.plist +++ /dev/null @@ -1,10 +0,0 @@ - - - - - LastAccessedDate - 2024-10-30T17:04:35Z - WorkspacePath - /Users/akmz/Developer/web/syncrow-web/web/macos/Runner.xcworkspace - - diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig deleted file mode 100644 index 4b81f9b2..00000000 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig deleted file mode 100644 index 5caa9d15..00000000 --- a/macos/Flutter/Flutter-Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" -#include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift deleted file mode 100644 index e6f3527d..00000000 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// Generated file. Do not edit. -// - -import FlutterMacOS -import Foundation - -import firebase_analytics -import firebase_core -import firebase_crashlytics -import firebase_database -import flutter_secure_storage_macos -import path_provider_foundation -import shared_preferences_foundation -import url_launcher_macos - -func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { - FLTFirebaseAnalyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAnalyticsPlugin")) - FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) - FLTFirebaseCrashlyticsPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCrashlyticsPlugin")) - FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin")) - FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) - PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) - SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) -} diff --git a/macos/Podfile b/macos/Podfile deleted file mode 100644 index c795730d..00000000 --- a/macos/Podfile +++ /dev/null @@ -1,43 +0,0 @@ -platform :osx, '10.14' - -# CocoaPods analytics sends network stats synchronously affecting flutter build latency. -ENV['COCOAPODS_DISABLE_STATS'] = 'true' - -project 'Runner', { - 'Debug' => :debug, - 'Profile' => :release, - 'Release' => :release, -} - -def flutter_root - generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) - unless File.exist?(generated_xcode_build_settings_path) - raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" - end - - File.foreach(generated_xcode_build_settings_path) do |line| - matches = line.match(/FLUTTER_ROOT\=(.*)/) - return matches[1].strip if matches - end - raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" -end - -require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) - -flutter_macos_podfile_setup - -target 'Runner' do - use_frameworks! - use_modular_headers! - - flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) - target 'RunnerTests' do - inherit! :search_paths - end -end - -post_install do |installer| - installer.pods_project.targets.each do |target| - flutter_additional_macos_build_settings(target) - end -end diff --git a/macos/Podfile.lock b/macos/Podfile.lock deleted file mode 100644 index 0639648b..00000000 --- a/macos/Podfile.lock +++ /dev/null @@ -1,36 +0,0 @@ -PODS: - - flutter_secure_storage_macos (6.1.1): - - FlutterMacOS - - FlutterMacOS (1.0.0) - - path_provider_foundation (0.0.1): - - Flutter - - FlutterMacOS - - shared_preferences_foundation (0.0.1): - - Flutter - - FlutterMacOS - -DEPENDENCIES: - - flutter_secure_storage_macos (from `Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos`) - - FlutterMacOS (from `Flutter/ephemeral`) - - path_provider_foundation (from `Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin`) - - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) - -EXTERNAL SOURCES: - flutter_secure_storage_macos: - :path: Flutter/ephemeral/.symlinks/plugins/flutter_secure_storage_macos/macos - FlutterMacOS: - :path: Flutter/ephemeral - path_provider_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/path_provider_foundation/darwin - shared_preferences_foundation: - :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin - -SPEC CHECKSUMS: - flutter_secure_storage_macos: 59459653abe1adb92abbc8ea747d79f8d19866c9 - FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 - path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 - shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 - -PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 - -COCOAPODS: 1.16.2 diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj deleted file mode 100644 index 7bfb45ff..00000000 --- a/macos/Runner.xcodeproj/project.pbxproj +++ /dev/null @@ -1,824 +0,0 @@ -// !$*UTF8*$! -{ - archiveVersion = 1; - classes = { - }; - objectVersion = 54; - objects = { - -/* Begin PBXAggregateTarget section */ - 33CC111A2044C6BA0003C045 /* Flutter Assemble */ = { - isa = PBXAggregateTarget; - buildConfigurationList = 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */; - buildPhases = ( - 33CC111E2044C6BF0003C045 /* ShellScript */, - ); - dependencies = ( - ); - name = "Flutter Assemble"; - productName = FLX; - }; -/* End PBXAggregateTarget section */ - -/* Begin PBXBuildFile section */ - 108157F896CD9F637B06D7C0 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 5DAF1C60594A51D692304366 /* Pods_Runner.framework */; }; - 2901225E5FAB0C696EE79F77 /* GoogleService-Info.plist in Resources */ = {isa = PBXBuildFile; fileRef = 996A2A515D007C9FED5396A5 /* GoogleService-Info.plist */; }; - 2D0F1F294F673EF0DB5E4CA1 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = E148CBDFFE42BF88E8C34DE0 /* Pods_RunnerTests.framework */; }; - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C80D7294CF71000263BE5 /* RunnerTests.swift */; }; - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */; }; - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC10F02044A3C60003C045 /* AppDelegate.swift */; }; - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; -/* End PBXBuildFile section */ - -/* Begin PBXContainerItemProxy section */ - 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC10EC2044A3C60003C045; - remoteInfo = Runner; - }; - 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */ = { - isa = PBXContainerItemProxy; - containerPortal = 33CC10E52044A3C60003C045 /* Project object */; - proxyType = 1; - remoteGlobalIDString = 33CC111A2044C6BA0003C045; - remoteInfo = FLX; - }; -/* End PBXContainerItemProxy section */ - -/* Begin PBXCopyFilesBuildPhase section */ - 33CC110E2044A8840003C045 /* Bundle Framework */ = { - isa = PBXCopyFilesBuildPhase; - buildActionMask = 2147483647; - dstPath = ""; - dstSubfolderSpec = 10; - files = ( - ); - name = "Bundle Framework"; - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXCopyFilesBuildPhase section */ - -/* Begin PBXFileReference section */ - 24D7BEF98D33245EFB9F6A1B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; - 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* syncrow_web.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = syncrow_web.app; sourceTree = BUILT_PRODUCTS_DIR; }; - 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; - 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; - 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; - 33CC10F72044A3C60003C045 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; name = Info.plist; path = Runner/Info.plist; sourceTree = ""; }; - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainFlutterWindow.swift; sourceTree = ""; }; - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Debug.xcconfig"; sourceTree = ""; }; - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = "Flutter-Release.xcconfig"; sourceTree = ""; }; - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = "Flutter-Generated.xcconfig"; path = "ephemeral/Flutter-Generated.xcconfig"; sourceTree = ""; }; - 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; - 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; - 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; - 5DAF1C60594A51D692304366 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; - 81F2F315AC5109F6F5D27BE6 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; - 96C46007EE0A4E9E1D6D74CE /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; - 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; - 996A2A515D007C9FED5396A5 /* GoogleService-Info.plist */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.plist.xml; name = "GoogleService-Info.plist"; path = "Runner/GoogleService-Info.plist"; sourceTree = ""; }; - A604E311B663FBF4B7C54DC5 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; - AB949539E0D0A8E2BDAB9ADF /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; - E148CBDFFE42BF88E8C34DE0 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; - F244F079A053D959E1C5C362 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; -/* End PBXFileReference section */ - -/* Begin PBXFrameworksBuildPhase section */ - 331C80D2294CF70F00263BE5 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 2D0F1F294F673EF0DB5E4CA1 /* Pods_RunnerTests.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EA2044A3C60003C045 /* Frameworks */ = { - isa = PBXFrameworksBuildPhase; - buildActionMask = 2147483647; - files = ( - 108157F896CD9F637B06D7C0 /* Pods_Runner.framework in Frameworks */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXFrameworksBuildPhase section */ - -/* Begin PBXGroup section */ - 331C80D6294CF71000263BE5 /* RunnerTests */ = { - isa = PBXGroup; - children = ( - 331C80D7294CF71000263BE5 /* RunnerTests.swift */, - ); - path = RunnerTests; - sourceTree = ""; - }; - 33BA886A226E78AF003329D5 /* Configs */ = { - isa = PBXGroup; - children = ( - 33E5194F232828860026EE4D /* AppInfo.xcconfig */, - 9740EEB21CF90195004384FC /* Debug.xcconfig */, - 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, - 333000ED22D3DE5D00554162 /* Warnings.xcconfig */, - ); - path = Configs; - sourceTree = ""; - }; - 33CC10E42044A3C60003C045 = { - isa = PBXGroup; - children = ( - 33FAB671232836740065AC1E /* Runner */, - 33CEB47122A05771004F2AC0 /* Flutter */, - 331C80D6294CF71000263BE5 /* RunnerTests */, - 33CC10EE2044A3C60003C045 /* Products */, - D73912EC22F37F3D000D13A0 /* Frameworks */, - 75DCDFECC7757C5159E8F0C5 /* Pods */, - 996A2A515D007C9FED5396A5 /* GoogleService-Info.plist */, - ); - sourceTree = ""; - }; - 33CC10EE2044A3C60003C045 /* Products */ = { - isa = PBXGroup; - children = ( - 33CC10ED2044A3C60003C045 /* syncrow_web.app */, - 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, - ); - name = Products; - sourceTree = ""; - }; - 33CC11242044D66E0003C045 /* Resources */ = { - isa = PBXGroup; - children = ( - 33CC10F22044A3C60003C045 /* Assets.xcassets */, - 33CC10F42044A3C60003C045 /* MainMenu.xib */, - 33CC10F72044A3C60003C045 /* Info.plist */, - ); - name = Resources; - path = ..; - sourceTree = ""; - }; - 33CEB47122A05771004F2AC0 /* Flutter */ = { - isa = PBXGroup; - children = ( - 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */, - 33CEB47222A05771004F2AC0 /* Flutter-Debug.xcconfig */, - 33CEB47422A05771004F2AC0 /* Flutter-Release.xcconfig */, - 33CEB47722A0578A004F2AC0 /* Flutter-Generated.xcconfig */, - ); - path = Flutter; - sourceTree = ""; - }; - 33FAB671232836740065AC1E /* Runner */ = { - isa = PBXGroup; - children = ( - 33CC10F02044A3C60003C045 /* AppDelegate.swift */, - 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */, - 33E51913231747F40026EE4D /* DebugProfile.entitlements */, - 33E51914231749380026EE4D /* Release.entitlements */, - 33CC11242044D66E0003C045 /* Resources */, - 33BA886A226E78AF003329D5 /* Configs */, - ); - path = Runner; - sourceTree = ""; - }; - 75DCDFECC7757C5159E8F0C5 /* Pods */ = { - isa = PBXGroup; - children = ( - 24D7BEF98D33245EFB9F6A1B /* Pods-Runner.debug.xcconfig */, - F244F079A053D959E1C5C362 /* Pods-Runner.release.xcconfig */, - AB949539E0D0A8E2BDAB9ADF /* Pods-Runner.profile.xcconfig */, - 96C46007EE0A4E9E1D6D74CE /* Pods-RunnerTests.debug.xcconfig */, - A604E311B663FBF4B7C54DC5 /* Pods-RunnerTests.release.xcconfig */, - 81F2F315AC5109F6F5D27BE6 /* Pods-RunnerTests.profile.xcconfig */, - ); - name = Pods; - path = Pods; - sourceTree = ""; - }; - D73912EC22F37F3D000D13A0 /* Frameworks */ = { - isa = PBXGroup; - children = ( - 5DAF1C60594A51D692304366 /* Pods_Runner.framework */, - E148CBDFFE42BF88E8C34DE0 /* Pods_RunnerTests.framework */, - ); - name = Frameworks; - sourceTree = ""; - }; -/* End PBXGroup section */ - -/* Begin PBXNativeTarget section */ - 331C80D4294CF70F00263BE5 /* RunnerTests */ = { - isa = PBXNativeTarget; - buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; - buildPhases = ( - A1935203066F42991FF0ED43 /* [CP] Check Pods Manifest.lock */, - 331C80D1294CF70F00263BE5 /* Sources */, - 331C80D2294CF70F00263BE5 /* Frameworks */, - 331C80D3294CF70F00263BE5 /* Resources */, - ); - buildRules = ( - ); - dependencies = ( - 331C80DA294CF71000263BE5 /* PBXTargetDependency */, - ); - name = RunnerTests; - productName = RunnerTests; - productReference = 331C80D5294CF71000263BE5 /* RunnerTests.xctest */; - productType = "com.apple.product-type.bundle.unit-test"; - }; - 33CC10EC2044A3C60003C045 /* Runner */ = { - isa = PBXNativeTarget; - buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; - buildPhases = ( - 8ECFD939A4D371A145DBA191 /* [CP] Check Pods Manifest.lock */, - 33CC10E92044A3C60003C045 /* Sources */, - 33CC10EA2044A3C60003C045 /* Frameworks */, - 33CC10EB2044A3C60003C045 /* Resources */, - 33CC110E2044A8840003C045 /* Bundle Framework */, - 3399D490228B24CF009A79C7 /* ShellScript */, - 92D754792F50A5D35F6D5AEE /* [CP] Embed Pods Frameworks */, - 7E188D2155D07A3E9E027C0F /* FlutterFire: "flutterfire upload-crashlytics-symbols" */, - ); - buildRules = ( - ); - dependencies = ( - 33CC11202044C79F0003C045 /* PBXTargetDependency */, - ); - name = Runner; - productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* syncrow_web.app */; - productType = "com.apple.product-type.application"; - }; -/* End PBXNativeTarget section */ - -/* Begin PBXProject section */ - 33CC10E52044A3C60003C045 /* Project object */ = { - isa = PBXProject; - attributes = { - BuildIndependentTargetsInParallel = YES; - LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1510; - ORGANIZATIONNAME = ""; - TargetAttributes = { - 331C80D4294CF70F00263BE5 = { - CreatedOnToolsVersion = 14.0; - TestTargetID = 33CC10EC2044A3C60003C045; - }; - 33CC10EC2044A3C60003C045 = { - CreatedOnToolsVersion = 9.2; - LastSwiftMigration = 1100; - ProvisioningStyle = Automatic; - SystemCapabilities = { - com.apple.Sandbox = { - enabled = 1; - }; - }; - }; - 33CC111A2044C6BA0003C045 = { - CreatedOnToolsVersion = 9.2; - ProvisioningStyle = Manual; - }; - }; - }; - buildConfigurationList = 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 9.3"; - developmentRegion = en; - hasScannedForEncodings = 0; - knownRegions = ( - en, - Base, - ); - mainGroup = 33CC10E42044A3C60003C045; - productRefGroup = 33CC10EE2044A3C60003C045 /* Products */; - projectDirPath = ""; - projectRoot = ""; - targets = ( - 33CC10EC2044A3C60003C045 /* Runner */, - 331C80D4294CF70F00263BE5 /* RunnerTests */, - 33CC111A2044C6BA0003C045 /* Flutter Assemble */, - ); - }; -/* End PBXProject section */ - -/* Begin PBXResourcesBuildPhase section */ - 331C80D3294CF70F00263BE5 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10EB2044A3C60003C045 /* Resources */ = { - isa = PBXResourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */, - 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */, - 2901225E5FAB0C696EE79F77 /* GoogleService-Info.plist in Resources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXResourcesBuildPhase section */ - -/* Begin PBXShellScriptBuildPhase section */ - 3399D490228B24CF009A79C7 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - alwaysOutOfDate = 1; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "echo \"$PRODUCT_NAME.app\" > \"$PROJECT_DIR\"/Flutter/ephemeral/.app_filename && \"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh embed\n"; - }; - 33CC111E2044C6BF0003C045 /* ShellScript */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - Flutter/ephemeral/FlutterInputs.xcfilelist, - ); - inputPaths = ( - Flutter/ephemeral/tripwire, - ); - outputFileListPaths = ( - Flutter/ephemeral/FlutterOutputs.xcfilelist, - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; - }; - 7E188D2155D07A3E9E027C0F /* FlutterFire: "flutterfire upload-crashlytics-symbols" */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - ); - name = "FlutterFire: \"flutterfire upload-crashlytics-symbols\""; - outputFileListPaths = ( - ); - outputPaths = ( - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\n#!/bin/bash\nPATH=\"${PATH}:$FLUTTER_ROOT/bin:$HOME/.pub-cache/bin\"\nflutterfire upload-crashlytics-symbols --upload-symbols-script-path=\"$PODS_ROOT/FirebaseCrashlytics/upload-symbols\" --platform=macos --apple-project-path=\"${SRCROOT}\" --env-platform-name=\"${PLATFORM_NAME}\" --env-configuration=\"${CONFIGURATION}\" --env-project-dir=\"${PROJECT_DIR}\" --env-built-products-dir=\"${BUILT_PRODUCTS_DIR}\" --env-dwarf-dsym-folder-path=\"${DWARF_DSYM_FOLDER_PATH}\" --env-dwarf-dsym-file-name=\"${DWARF_DSYM_FILE_NAME}\" --env-infoplist-path=\"${INFOPLIST_PATH}\" --default-config=default\n"; - }; - 8ECFD939A4D371A145DBA191 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; - 92D754792F50A5D35F6D5AEE /* [CP] Embed Pods Frameworks */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", - ); - name = "[CP] Embed Pods Frameworks"; - outputFileListPaths = ( - "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; - showEnvVarsInLog = 0; - }; - A1935203066F42991FF0ED43 /* [CP] Check Pods Manifest.lock */ = { - isa = PBXShellScriptBuildPhase; - buildActionMask = 2147483647; - files = ( - ); - inputFileListPaths = ( - ); - inputPaths = ( - "${PODS_PODFILE_DIR_PATH}/Podfile.lock", - "${PODS_ROOT}/Manifest.lock", - ); - name = "[CP] Check Pods Manifest.lock"; - outputFileListPaths = ( - ); - outputPaths = ( - "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", - ); - runOnlyForDeploymentPostprocessing = 0; - shellPath = /bin/sh; - shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; - showEnvVarsInLog = 0; - }; -/* End PBXShellScriptBuildPhase section */ - -/* Begin PBXSourcesBuildPhase section */ - 331C80D1294CF70F00263BE5 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 331C80D8294CF71000263BE5 /* RunnerTests.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; - 33CC10E92044A3C60003C045 /* Sources */ = { - isa = PBXSourcesBuildPhase; - buildActionMask = 2147483647; - files = ( - 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */, - 33CC10F12044A3C60003C045 /* AppDelegate.swift in Sources */, - 335BBD1B22A9A15E00E9071D /* GeneratedPluginRegistrant.swift in Sources */, - ); - runOnlyForDeploymentPostprocessing = 0; - }; -/* End PBXSourcesBuildPhase section */ - -/* Begin PBXTargetDependency section */ - 331C80DA294CF71000263BE5 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC10EC2044A3C60003C045 /* Runner */; - targetProxy = 331C80D9294CF71000263BE5 /* PBXContainerItemProxy */; - }; - 33CC11202044C79F0003C045 /* PBXTargetDependency */ = { - isa = PBXTargetDependency; - target = 33CC111A2044C6BA0003C045 /* Flutter Assemble */; - targetProxy = 33CC111F2044C79F0003C045 /* PBXContainerItemProxy */; - }; -/* End PBXTargetDependency section */ - -/* Begin PBXVariantGroup section */ - 33CC10F42044A3C60003C045 /* MainMenu.xib */ = { - isa = PBXVariantGroup; - children = ( - 33CC10F52044A3C60003C045 /* Base */, - ); - name = MainMenu.xib; - path = Runner; - sourceTree = ""; - }; -/* End PBXVariantGroup section */ - -/* Begin XCBuildConfiguration section */ - 331C80DB294CF71000263BE5 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 96C46007EE0A4E9E1D6D74CE /* Pods-RunnerTests.debug.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.syncrowWeb.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/syncrow_web.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/syncrow_web"; - }; - name = Debug; - }; - 331C80DC294CF71000263BE5 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = A604E311B663FBF4B7C54DC5 /* Pods-RunnerTests.release.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.syncrowWeb.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/syncrow_web.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/syncrow_web"; - }; - name = Release; - }; - 331C80DD294CF71000263BE5 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 81F2F315AC5109F6F5D27BE6 /* Pods-RunnerTests.profile.xcconfig */; - buildSettings = { - BUNDLE_LOADER = "$(TEST_HOST)"; - CURRENT_PROJECT_VERSION = 1; - GENERATE_INFOPLIST_FILE = YES; - MARKETING_VERSION = 1.0; - PRODUCT_BUNDLE_IDENTIFIER = com.example.syncrowWeb.RunnerTests; - PRODUCT_NAME = "$(TARGET_NAME)"; - SWIFT_VERSION = 5.0; - TEST_HOST = "$(BUILT_PRODUCTS_DIR)/syncrow_web.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/syncrow_web"; - }; - name = Profile; - }; - 338D0CE9231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Profile; - }; - 338D0CEA231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Profile; - }; - 338D0CEB231458BD00FA5F75 /* Profile */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Profile; - }; - 33CC10F92044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = dwarf; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_TESTABILITY = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_DYNAMIC_NO_PIC = NO; - GCC_NO_COMMON_BLOCKS = YES; - GCC_OPTIMIZATION_LEVEL = 0; - GCC_PREPROCESSOR_DEFINITIONS = ( - "DEBUG=1", - "$(inherited)", - ); - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = YES; - ONLY_ACTIVE_ARCH = YES; - SDKROOT = macosx; - SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - }; - name = Debug; - }; - 33CC10FA2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; - buildSettings = { - ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; - CLANG_ANALYZER_NONNULL = YES; - CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; - CLANG_CXX_LANGUAGE_STANDARD = "gnu++14"; - CLANG_CXX_LIBRARY = "libc++"; - CLANG_ENABLE_MODULES = YES; - CLANG_ENABLE_OBJC_ARC = YES; - CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; - CLANG_WARN_BOOL_CONVERSION = YES; - CLANG_WARN_CONSTANT_CONVERSION = YES; - CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; - CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; - CLANG_WARN_DOCUMENTATION_COMMENTS = YES; - CLANG_WARN_EMPTY_BODY = YES; - CLANG_WARN_ENUM_CONVERSION = YES; - CLANG_WARN_INFINITE_RECURSION = YES; - CLANG_WARN_INT_CONVERSION = YES; - CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; - CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; - CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; - CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; - CLANG_WARN_SUSPICIOUS_MOVE = YES; - CODE_SIGN_IDENTITY = "-"; - COPY_PHASE_STRIP = NO; - DEAD_CODE_STRIPPING = YES; - DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; - ENABLE_NS_ASSERTIONS = NO; - ENABLE_STRICT_OBJC_MSGSEND = YES; - ENABLE_USER_SCRIPT_SANDBOXING = NO; - GCC_C_LANGUAGE_STANDARD = gnu11; - GCC_NO_COMMON_BLOCKS = YES; - GCC_WARN_64_TO_32_BIT_CONVERSION = YES; - GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; - GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; - GCC_WARN_UNUSED_FUNCTION = YES; - GCC_WARN_UNUSED_VARIABLE = YES; - MACOSX_DEPLOYMENT_TARGET = 10.14; - MTL_ENABLE_DEBUG_INFO = NO; - SDKROOT = macosx; - SWIFT_COMPILATION_MODE = wholemodule; - SWIFT_OPTIMIZATION_LEVEL = "-O"; - }; - name = Release; - }; - 33CC10FC2044A3C60003C045 /* Debug */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_OPTIMIZATION_LEVEL = "-Onone"; - SWIFT_VERSION = 5.0; - }; - name = Debug; - }; - 33CC10FD2044A3C60003C045 /* Release */ = { - isa = XCBuildConfiguration; - baseConfigurationReference = 33E5194F232828860026EE4D /* AppInfo.xcconfig */; - buildSettings = { - ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; - CLANG_ENABLE_MODULES = YES; - CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; - COMBINE_HIDPI_IMAGES = YES; - INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = ( - "$(inherited)", - "@executable_path/../Frameworks", - ); - PROVISIONING_PROFILE_SPECIFIER = ""; - SWIFT_VERSION = 5.0; - }; - name = Release; - }; - 33CC111C2044C6BA0003C045 /* Debug */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Manual; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Debug; - }; - 33CC111D2044C6BA0003C045 /* Release */ = { - isa = XCBuildConfiguration; - buildSettings = { - CODE_SIGN_STYLE = Automatic; - PRODUCT_NAME = "$(TARGET_NAME)"; - }; - name = Release; - }; -/* End XCBuildConfiguration section */ - -/* Begin XCConfigurationList section */ - 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 331C80DB294CF71000263BE5 /* Debug */, - 331C80DC294CF71000263BE5 /* Release */, - 331C80DD294CF71000263BE5 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10E82044A3C60003C045 /* Build configuration list for PBXProject "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10F92044A3C60003C045 /* Debug */, - 33CC10FA2044A3C60003C045 /* Release */, - 338D0CE9231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC10FC2044A3C60003C045 /* Debug */, - 33CC10FD2044A3C60003C045 /* Release */, - 338D0CEA231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; - 33CC111B2044C6BA0003C045 /* Build configuration list for PBXAggregateTarget "Flutter Assemble" */ = { - isa = XCConfigurationList; - buildConfigurations = ( - 33CC111C2044C6BA0003C045 /* Debug */, - 33CC111D2044C6BA0003C045 /* Release */, - 338D0CEB231458BD00FA5F75 /* Profile */, - ); - defaultConfigurationIsVisible = 0; - defaultConfigurationName = Release; - }; -/* End XCConfigurationList section */ - }; - rootObject = 33CC10E52044A3C60003C045 /* Project object */; -} diff --git a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme deleted file mode 100644 index f16dfbdc..00000000 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ /dev/null @@ -1,98 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata deleted file mode 100644 index 21a3cc14..00000000 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - diff --git a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist deleted file mode 100644 index 18d98100..00000000 --- a/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist +++ /dev/null @@ -1,8 +0,0 @@ - - - - - IDEDidComputeMac32BitWarning - - - diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift deleted file mode 100644 index d53ef643..00000000 --- a/macos/Runner/AppDelegate.swift +++ /dev/null @@ -1,9 +0,0 @@ -import Cocoa -import FlutterMacOS - -@NSApplicationMain -class AppDelegate: FlutterAppDelegate { - override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { - return true - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json deleted file mode 100644 index a2ec33f1..00000000 --- a/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ /dev/null @@ -1,68 +0,0 @@ -{ - "images" : [ - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_16.png", - "scale" : "1x" - }, - { - "size" : "16x16", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "2x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_32.png", - "scale" : "1x" - }, - { - "size" : "32x32", - "idiom" : "mac", - "filename" : "app_icon_64.png", - "scale" : "2x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_128.png", - "scale" : "1x" - }, - { - "size" : "128x128", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "2x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_256.png", - "scale" : "1x" - }, - { - "size" : "256x256", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "2x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_512.png", - "scale" : "1x" - }, - { - "size" : "512x512", - "idiom" : "mac", - "filename" : "app_icon_1024.png", - "scale" : "2x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png deleted file mode 100644 index 82b6f9d9..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png deleted file mode 100644 index 13b35eba..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png deleted file mode 100644 index 0a3f5fa4..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png deleted file mode 100644 index bdb57226..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png deleted file mode 100644 index f083318e..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png deleted file mode 100644 index 326c0e72..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png and /dev/null differ diff --git a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png b/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png deleted file mode 100644 index 2f1632cf..00000000 Binary files a/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png and /dev/null differ diff --git a/macos/Runner/Base.lproj/MainMenu.xib b/macos/Runner/Base.lproj/MainMenu.xib deleted file mode 100644 index 80e867a4..00000000 --- a/macos/Runner/Base.lproj/MainMenu.xib +++ /dev/null @@ -1,343 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/macos/Runner/Configs/AppInfo.xcconfig b/macos/Runner/Configs/AppInfo.xcconfig deleted file mode 100644 index ec06430f..00000000 --- a/macos/Runner/Configs/AppInfo.xcconfig +++ /dev/null @@ -1,14 +0,0 @@ -// Application-level settings for the Runner target. -// -// This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the -// future. If not, the values below would default to using the project name when this becomes a -// 'flutter create' template. - -// The application's name. By default this is also the title of the Flutter window. -PRODUCT_NAME = syncrow_web - -// The application's bundle identifier -PRODUCT_BUNDLE_IDENTIFIER = com.example.syncrowWeb - -// The copyright displayed in application information -PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. diff --git a/macos/Runner/Configs/Debug.xcconfig b/macos/Runner/Configs/Debug.xcconfig deleted file mode 100644 index 36b0fd94..00000000 --- a/macos/Runner/Configs/Debug.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Debug.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Release.xcconfig b/macos/Runner/Configs/Release.xcconfig deleted file mode 100644 index dff4f495..00000000 --- a/macos/Runner/Configs/Release.xcconfig +++ /dev/null @@ -1,2 +0,0 @@ -#include "../../Flutter/Flutter-Release.xcconfig" -#include "Warnings.xcconfig" diff --git a/macos/Runner/Configs/Warnings.xcconfig b/macos/Runner/Configs/Warnings.xcconfig deleted file mode 100644 index 42bcbf47..00000000 --- a/macos/Runner/Configs/Warnings.xcconfig +++ /dev/null @@ -1,13 +0,0 @@ -WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings -GCC_WARN_UNDECLARED_SELECTOR = YES -CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES -CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE -CLANG_WARN__DUPLICATE_METHOD_MATCH = YES -CLANG_WARN_PRAGMA_PACK = YES -CLANG_WARN_STRICT_PROTOTYPES = YES -CLANG_WARN_COMMA = YES -GCC_WARN_STRICT_SELECTOR_MATCH = YES -CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES -CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES -GCC_WARN_SHADOW = YES -CLANG_WARN_UNREACHABLE_CODE = YES diff --git a/macos/Runner/DebugProfile.entitlements b/macos/Runner/DebugProfile.entitlements deleted file mode 100644 index dddb8a30..00000000 --- a/macos/Runner/DebugProfile.entitlements +++ /dev/null @@ -1,12 +0,0 @@ - - - - - com.apple.security.app-sandbox - - com.apple.security.cs.allow-jit - - com.apple.security.network.server - - - diff --git a/macos/Runner/GoogleService-Info.plist b/macos/Runner/GoogleService-Info.plist deleted file mode 100644 index 9cdebed0..00000000 --- a/macos/Runner/GoogleService-Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - API_KEY - AIzaSyABnpH6yo2RRjtkp4PlvtK84hKwRm2DhBw - GCM_SENDER_ID - 427332280600 - PLIST_VERSION - 1 - BUNDLE_ID - com.example.syncrowWeb - PROJECT_ID - test2-8a3d2 - STORAGE_BUCKET - test2-8a3d2.firebasestorage.app - IS_ADS_ENABLED - - IS_ANALYTICS_ENABLED - - IS_APPINVITE_ENABLED - - IS_GCM_ENABLED - - IS_SIGNIN_ENABLED - - GOOGLE_APP_ID - 1:427332280600:ios:14346b200780dc760c7e6d - DATABASE_URL - https://test2-8a3d2-default-rtdb.firebaseio.com - - \ No newline at end of file diff --git a/macos/Runner/Info.plist b/macos/Runner/Info.plist deleted file mode 100644 index 4789daa6..00000000 --- a/macos/Runner/Info.plist +++ /dev/null @@ -1,32 +0,0 @@ - - - - - CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) - CFBundleExecutable - $(EXECUTABLE_NAME) - CFBundleIconFile - - CFBundleIdentifier - $(PRODUCT_BUNDLE_IDENTIFIER) - CFBundleInfoDictionaryVersion - 6.0 - CFBundleName - $(PRODUCT_NAME) - CFBundlePackageType - APPL - CFBundleShortVersionString - $(FLUTTER_BUILD_NAME) - CFBundleVersion - $(FLUTTER_BUILD_NUMBER) - LSMinimumSystemVersion - $(MACOSX_DEPLOYMENT_TARGET) - NSHumanReadableCopyright - $(PRODUCT_COPYRIGHT) - NSMainNibFile - MainMenu - NSPrincipalClass - NSApplication - - diff --git a/macos/Runner/MainFlutterWindow.swift b/macos/Runner/MainFlutterWindow.swift deleted file mode 100644 index 3cc05eb2..00000000 --- a/macos/Runner/MainFlutterWindow.swift +++ /dev/null @@ -1,15 +0,0 @@ -import Cocoa -import FlutterMacOS - -class MainFlutterWindow: NSWindow { - override func awakeFromNib() { - let flutterViewController = FlutterViewController() - let windowFrame = self.frame - self.contentViewController = flutterViewController - self.setFrame(windowFrame, display: true) - - RegisterGeneratedPlugins(registry: flutterViewController) - - super.awakeFromNib() - } -} diff --git a/macos/Runner/Release.entitlements b/macos/Runner/Release.entitlements deleted file mode 100644 index 852fa1a4..00000000 --- a/macos/Runner/Release.entitlements +++ /dev/null @@ -1,8 +0,0 @@ - - - - - com.apple.security.app-sandbox - - - diff --git a/macos/RunnerTests/RunnerTests.swift b/macos/RunnerTests/RunnerTests.swift deleted file mode 100644 index 5418c9f5..00000000 --- a/macos/RunnerTests/RunnerTests.swift +++ /dev/null @@ -1,12 +0,0 @@ -import FlutterMacOS -import Cocoa -import XCTest - -class RunnerTests: XCTestCase { - - func testExample() { - // If you add code to the Runner application, consider adding tests here. - // See https://developer.apple.com/documentation/xctest for more information about using XCTest. - } - -} diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index fb0d6a22..00000000 --- a/pubspec.lock +++ /dev/null @@ -1,906 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - _flutterfire_internals: - dependency: transitive - description: - name: _flutterfire_internals - sha256: e051259913915ea5bc8fe18664596bea08592fd123930605d562969cd7315fcd - url: "https://pub.dev" - source: hosted - version: "1.3.51" - args: - dependency: transitive - description: - name: args - sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" - url: "https://pub.dev" - source: hosted - version: "2.5.0" - async: - dependency: transitive - description: - name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" - url: "https://pub.dev" - source: hosted - version: "2.11.0" - bloc: - dependency: "direct main" - description: - name: bloc - sha256: "106842ad6569f0b60297619e9e0b1885c2fb9bf84812935490e6c5275777804e" - url: "https://pub.dev" - source: hosted - version: "8.1.4" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" - url: "https://pub.dev" - source: hosted - version: "2.1.1" - characters: - dependency: transitive - description: - name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - clock: - dependency: transitive - description: - name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf - url: "https://pub.dev" - source: hosted - version: "1.1.1" - collection: - dependency: transitive - description: - name: collection - sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf - url: "https://pub.dev" - source: hosted - version: "1.19.0" - crypto: - dependency: transitive - description: - name: crypto - sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" - url: "https://pub.dev" - source: hosted - version: "3.0.6" - csslib: - dependency: transitive - description: - name: csslib - sha256: "831883fb353c8bdc1d71979e5b342c7d88acfbc643113c14ae51e2442ea0f20f" - url: "https://pub.dev" - source: hosted - version: "0.17.3" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 - url: "https://pub.dev" - source: hosted - version: "1.0.8" - data_table_2: - dependency: "direct main" - description: - name: data_table_2 - sha256: f02ec9b24f44420816a87370ff4f4e533e15b274f6267e4c9a88a585ad1a0473 - url: "https://pub.dev" - source: hosted - version: "2.5.15" - dio: - dependency: "direct main" - description: - name: dio - sha256: e17f6b3097b8c51b72c74c9f071a605c47bcc8893839bd66732457a5ebe73714 - url: "https://pub.dev" - source: hosted - version: "5.5.0+1" - dio_web_adapter: - dependency: transitive - description: - name: dio_web_adapter - sha256: "36c5b2d79eb17cdae41e974b7a8284fec631651d2a6f39a8a2ff22327e90aeac" - url: "https://pub.dev" - source: hosted - version: "1.0.1" - dropdown_button2: - dependency: "direct main" - description: - name: dropdown_button2 - sha256: b0fe8d49a030315e9eef6c7ac84ca964250155a6224d491c1365061bc974a9e1 - url: "https://pub.dev" - source: hosted - version: "2.3.9" - dropdown_search: - dependency: "direct main" - description: - name: dropdown_search - sha256: "55106e8290acaa97ed15bea1fdad82c3cf0c248dd410e651f5a8ac6870f783ab" - url: "https://pub.dev" - source: hosted - version: "5.0.6" - equatable: - dependency: "direct main" - description: - name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 - url: "https://pub.dev" - source: hosted - version: "2.0.5" - fake_async: - dependency: transitive - description: - name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" - url: "https://pub.dev" - source: hosted - version: "1.3.1" - ffi: - dependency: transitive - description: - name: ffi - sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - file: - dependency: transitive - description: - name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - firebase_analytics: - dependency: "direct main" - description: - name: firebase_analytics - sha256: "47428047a0778f72af53a3c7cb5d556e1cb25e2327cc8aa40d544971dc6245b2" - url: "https://pub.dev" - source: hosted - version: "11.4.2" - firebase_analytics_platform_interface: - dependency: transitive - description: - name: firebase_analytics_platform_interface - sha256: "1076f4b041f76143e14878c70f0758f17fe5910c0cd992db9e93bd3c3584512b" - url: "https://pub.dev" - source: hosted - version: "4.3.2" - firebase_analytics_web: - dependency: transitive - description: - name: firebase_analytics_web - sha256: "8f6dd64ea6d28b7f5b9e739d183a9e1c7f17027794a3e9aba1879621d42426ef" - url: "https://pub.dev" - source: hosted - version: "0.5.10+8" - firebase_core: - dependency: "direct main" - description: - name: firebase_core - sha256: "93dc4dd12f9b02c5767f235307f609e61ed9211047132d07f9e02c668f0bfc33" - url: "https://pub.dev" - source: hosted - version: "3.11.0" - firebase_core_platform_interface: - dependency: transitive - description: - name: firebase_core_platform_interface - sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf - url: "https://pub.dev" - source: hosted - version: "5.4.0" - firebase_core_web: - dependency: transitive - description: - name: firebase_core_web - sha256: "0e13c80f0de8acaa5d0519cbe23c8b4cc138a2d5d508b5755c861bdfc9762678" - url: "https://pub.dev" - source: hosted - version: "2.20.0" - firebase_crashlytics: - dependency: "direct main" - description: - name: firebase_crashlytics - sha256: "6273ed71bcd8a6fb4d0ca13d3abddbb3301796807efaad8782b5f90156f26f03" - url: "https://pub.dev" - source: hosted - version: "4.3.2" - firebase_crashlytics_platform_interface: - dependency: transitive - description: - name: firebase_crashlytics_platform_interface - sha256: "94f3986e1a10e5a883f2ad5e3d719aef98a8a0f9a49357f6e45b7d3696ea6a97" - url: "https://pub.dev" - source: hosted - version: "3.8.2" - firebase_database: - dependency: "direct main" - description: - name: firebase_database - sha256: cd2354dfef68e52c0713b5efbb7f4e10dfc2aff2f945c7bc8db34d1934170627 - url: "https://pub.dev" - source: hosted - version: "11.3.2" - firebase_database_platform_interface: - dependency: transitive - description: - name: firebase_database_platform_interface - sha256: d430983f4d877c9f72f88b3d715cca9a50021dd7ccd8e3ae6fb79603853317de - url: "https://pub.dev" - source: hosted - version: "0.2.6+2" - firebase_database_web: - dependency: transitive - description: - name: firebase_database_web - sha256: f64edae62c5beaa08e9e611a0736d64ab11a812983a0aa132695d2d191311ea7 - url: "https://pub.dev" - source: hosted - version: "0.2.6+8" - fixnum: - dependency: transitive - description: - name: fixnum - sha256: b6dc7065e46c974bc7c5f143080a6764ec7a4be6da1285ececdc37be96de53be - url: "https://pub.dev" - source: hosted - version: "1.1.1" - fl_chart: - dependency: "direct main" - description: - name: fl_chart - sha256: "94307bef3a324a0d329d3ab77b2f0c6e5ed739185ffc029ed28c0f9b019ea7ef" - url: "https://pub.dev" - source: hosted - version: "0.69.0" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_bloc: - dependency: "direct main" - description: - name: flutter_bloc - sha256: f0ecf6e6eb955193ca60af2d5ca39565a86b8a142452c5b24d96fb477428f4d2 - url: "https://pub.dev" - source: hosted - version: "8.1.5" - flutter_dotenv: - dependency: "direct main" - description: - name: flutter_dotenv - sha256: "9357883bdd153ab78cbf9ffa07656e336b8bbb2b5a3ca596b0b27e119f7c7d77" - url: "https://pub.dev" - source: hosted - version: "5.1.0" - flutter_html: - dependency: "direct main" - description: - name: flutter_html - sha256: "02ad69e813ecfc0728a455e4bf892b9379983e050722b1dce00192ee2e41d1ee" - url: "https://pub.dev" - source: hosted - version: "3.0.0-beta.2" - flutter_lints: - dependency: "direct dev" - description: - name: flutter_lints - sha256: "9e8c3858111da373efc5aa341de011d9bd23e2c5c5e0c62bccf32438e192d7b1" - url: "https://pub.dev" - source: hosted - version: "3.0.2" - flutter_secure_storage: - dependency: "direct main" - description: - name: flutter_secure_storage - sha256: "165164745e6afb5c0e3e3fcc72a012fb9e58496fb26ffb92cf22e16a821e85d0" - url: "https://pub.dev" - source: hosted - version: "9.2.2" - flutter_secure_storage_linux: - dependency: transitive - description: - name: flutter_secure_storage_linux - sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" - url: "https://pub.dev" - source: hosted - version: "1.2.1" - flutter_secure_storage_macos: - dependency: transitive - description: - name: flutter_secure_storage_macos - sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" - url: "https://pub.dev" - source: hosted - version: "3.1.2" - flutter_secure_storage_platform_interface: - dependency: transitive - description: - name: flutter_secure_storage_platform_interface - sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 - url: "https://pub.dev" - source: hosted - version: "1.1.2" - flutter_secure_storage_web: - dependency: transitive - description: - name: flutter_secure_storage_web - sha256: f4ebff989b4f07b2656fb16b47852c0aab9fed9b4ec1c70103368337bc1886a9 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - flutter_secure_storage_windows: - dependency: transitive - description: - name: flutter_secure_storage_windows - sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 - url: "https://pub.dev" - source: hosted - version: "3.1.2" - flutter_svg: - dependency: "direct main" - description: - name: flutter_svg - sha256: "7b4ca6cf3304575fe9c8ec64813c8d02ee41d2afe60bcfe0678bcb5375d596a2" - url: "https://pub.dev" - source: hosted - version: "2.0.10+1" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - get_it: - dependency: "direct main" - description: - name: get_it - sha256: d85128a5dae4ea777324730dc65edd9c9f43155c109d5cc0a69cab74139fbac1 - url: "https://pub.dev" - source: hosted - version: "7.7.0" - go_router: - dependency: "direct main" - description: - name: go_router - sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459" - url: "https://pub.dev" - source: hosted - version: "14.2.7" - graphview: - dependency: "direct main" - description: - name: graphview - sha256: bdba183583b23c30c71edea09ad5f0beef612572d3e39e855467a925bd08392f - url: "https://pub.dev" - source: hosted - version: "1.2.0" - html: - dependency: transitive - description: - name: html - sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" - url: "https://pub.dev" - source: hosted - version: "0.15.4" - http: - dependency: transitive - description: - name: http - sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 - url: "https://pub.dev" - source: hosted - version: "1.2.2" - http_parser: - dependency: transitive - description: - name: http_parser - sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b" - url: "https://pub.dev" - source: hosted - version: "4.0.2" - intl: - dependency: "direct main" - description: - name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf - url: "https://pub.dev" - source: hosted - version: "0.19.0" - intl_phone_field: - dependency: "direct main" - description: - name: intl_phone_field - sha256: "73819d3dfcb68d2c85663606f6842597c3ddf6688ac777f051b17814fe767bbf" - url: "https://pub.dev" - source: hosted - version: "3.2.0" - js: - dependency: transitive - description: - name: js - sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 - url: "https://pub.dev" - source: hosted - version: "0.6.7" - leak_tracker: - dependency: transitive - description: - name: leak_tracker - sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" - url: "https://pub.dev" - source: hosted - version: "10.0.7" - leak_tracker_flutter_testing: - dependency: transitive - description: - name: leak_tracker_flutter_testing - sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" - url: "https://pub.dev" - source: hosted - version: "3.0.8" - leak_tracker_testing: - dependency: transitive - description: - name: leak_tracker_testing - sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" - url: "https://pub.dev" - source: hosted - version: "3.0.1" - lints: - dependency: transitive - description: - name: lints - sha256: cbf8d4b858bb0134ef3ef87841abdf8d63bfc255c266b7bf6b39daa1085c4290 - url: "https://pub.dev" - source: hosted - version: "3.0.0" - list_counter: - dependency: transitive - description: - name: list_counter - sha256: c447ae3dfcd1c55f0152867090e67e219d42fe6d4f2807db4bbe8b8d69912237 - url: "https://pub.dev" - source: hosted - version: "1.0.2" - logging: - dependency: transitive - description: - name: logging - sha256: "623a88c9594aa774443aa3eb2d41807a48486b5613e67599fb4c41c0ad47c340" - url: "https://pub.dev" - source: hosted - version: "1.2.0" - matcher: - dependency: transitive - description: - name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb - url: "https://pub.dev" - source: hosted - version: "0.12.16+1" - material_color_utilities: - dependency: transitive - description: - name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec - url: "https://pub.dev" - source: hosted - version: "0.11.1" - meta: - dependency: transitive - description: - name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 - url: "https://pub.dev" - source: hosted - version: "1.15.0" - nested: - dependency: transitive - description: - name: nested - sha256: "03bac4c528c64c95c722ec99280375a6f2fc708eec17c7b3f07253b626cd2a20" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - number_pagination: - dependency: "direct main" - description: - name: number_pagination - sha256: "75d3a28616196e7c8df431d0fb7c48e811e462155f4cf3b5b4167b3408421327" - url: "https://pub.dev" - source: hosted - version: "1.1.6" - path: - dependency: transitive - description: - name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" - url: "https://pub.dev" - source: hosted - version: "1.9.0" - path_parsing: - dependency: transitive - description: - name: path_parsing - sha256: e3e67b1629e6f7e8100b367d3db6ba6af4b1f0bb80f64db18ef1fbabd2fa9ccf - url: "https://pub.dev" - source: hosted - version: "1.0.1" - path_provider: - dependency: transitive - description: - name: path_provider - sha256: c9e7d3a4cd1410877472158bee69963a4579f78b68c65a2b7d40d1a7a88bb161 - url: "https://pub.dev" - source: hosted - version: "2.1.3" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - sha256: e84c8a53fe1510ef4582f118c7b4bdf15b03002b51d7c2b66983c65843d61193 - url: "https://pub.dev" - source: hosted - version: "2.2.8" - path_provider_foundation: - dependency: transitive - description: - name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 - url: "https://pub.dev" - source: hosted - version: "2.4.0" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 - url: "https://pub.dev" - source: hosted - version: "2.2.1" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" - url: "https://pub.dev" - source: hosted - version: "2.1.2" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 - url: "https://pub.dev" - source: hosted - version: "2.3.0" - petitparser: - dependency: transitive - description: - name: petitparser - sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 - url: "https://pub.dev" - source: hosted - version: "6.0.2" - platform: - dependency: transitive - description: - name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" - url: "https://pub.dev" - source: hosted - version: "3.1.5" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" - url: "https://pub.dev" - source: hosted - version: "2.1.8" - provider: - dependency: transitive - description: - name: provider - sha256: c8a055ee5ce3fd98d6fc872478b03823ffdb448699c6ebdbbc71d59b596fd48c - url: "https://pub.dev" - source: hosted - version: "6.1.2" - shared_preferences: - dependency: "direct main" - description: - name: shared_preferences - sha256: c3f888ba2d659f3e75f4686112cc1e71f46177f74452d40d8307edc332296ead - url: "https://pub.dev" - source: hosted - version: "2.3.0" - shared_preferences_android: - dependency: transitive - description: - name: shared_preferences_android - sha256: "041be4d9d2dc6079cf342bc8b761b03787e3b71192d658220a56cac9c04a0294" - url: "https://pub.dev" - source: hosted - version: "2.3.0" - shared_preferences_foundation: - dependency: transitive - description: - name: shared_preferences_foundation - sha256: "671e7a931f55a08aa45be2a13fe7247f2a41237897df434b30d2012388191833" - url: "https://pub.dev" - source: hosted - version: "2.5.0" - shared_preferences_linux: - dependency: transitive - description: - name: shared_preferences_linux - sha256: "2ba0510d3017f91655b7543e9ee46d48619de2a2af38e5c790423f7007c7ccc1" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - sha256: d2ca4132d3946fec2184261726b355836a82c33d7d5b67af32692aff18a4684e - url: "https://pub.dev" - source: hosted - version: "2.4.2" - shared_preferences_windows: - dependency: transitive - description: - name: shared_preferences_windows - sha256: "398084b47b7f92110683cac45c6dc4aae853db47e470e5ddcd52cab7f7196ab2" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - source_span: - dependency: transitive - description: - name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" - url: "https://pub.dev" - source: hosted - version: "1.10.0" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - stack_trace: - dependency: transitive - description: - name: stack_trace - sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" - url: "https://pub.dev" - source: hosted - version: "1.12.0" - stream_channel: - dependency: transitive - description: - name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 - url: "https://pub.dev" - source: hosted - version: "2.1.2" - string_scanner: - dependency: transitive - description: - name: string_scanner - sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" - url: "https://pub.dev" - source: hosted - version: "1.3.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 - url: "https://pub.dev" - source: hosted - version: "1.2.1" - test_api: - dependency: transitive - description: - name: test_api - sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" - url: "https://pub.dev" - source: hosted - version: "0.7.3" - time_picker_spinner: - dependency: "direct main" - description: - name: time_picker_spinner - sha256: "53d824801d108890d22756501e7ade9db48b53dac1ec41580499dd4ebd128e3c" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - typed_data: - dependency: transitive - description: - name: typed_data - sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c - url: "https://pub.dev" - source: hosted - version: "1.3.2" - url_launcher: - dependency: "direct main" - description: - name: url_launcher - sha256: "9d06212b1362abc2f0f0d78e6f09f726608c74e3b9462e8368bb03314aa8d603" - url: "https://pub.dev" - source: hosted - version: "6.3.1" - url_launcher_android: - dependency: transitive - description: - name: url_launcher_android - sha256: "6fc2f56536ee873eeb867ad176ae15f304ccccc357848b351f6f0d8d4a40d193" - url: "https://pub.dev" - source: hosted - version: "6.3.14" - url_launcher_ios: - dependency: transitive - description: - name: url_launcher_ios - sha256: "16a513b6c12bb419304e72ea0ae2ab4fed569920d1c7cb850263fe3acc824626" - url: "https://pub.dev" - source: hosted - version: "6.3.2" - url_launcher_linux: - dependency: transitive - description: - name: url_launcher_linux - sha256: "4e9ba368772369e3e08f231d2301b4ef72b9ff87c31192ef471b380ef29a4935" - url: "https://pub.dev" - source: hosted - version: "3.2.1" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - sha256: "17ba2000b847f334f16626a574c702b196723af2a289e7a93ffcb79acff855c2" - url: "https://pub.dev" - source: hosted - version: "3.2.2" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" - url: "https://pub.dev" - source: hosted - version: "2.3.2" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - sha256: "3ba963161bd0fe395917ba881d320b9c4f6dd3c4a233da62ab18a5025c85f1e9" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - url_launcher_windows: - dependency: transitive - description: - name: url_launcher_windows - sha256: "3284b6d2ac454cf34f114e1d3319866fdd1e19cdc329999057e44ffe936cfa77" - url: "https://pub.dev" - source: hosted - version: "3.1.4" - uuid: - dependency: "direct main" - description: - name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff - url: "https://pub.dev" - source: hosted - version: "4.5.1" - vector_graphics: - dependency: transitive - description: - name: vector_graphics - sha256: "32c3c684e02f9bc0afb0ae0aa653337a2fe022e8ab064bcd7ffda27a74e288e3" - url: "https://pub.dev" - source: hosted - version: "1.1.11+1" - vector_graphics_codec: - dependency: transitive - description: - name: vector_graphics_codec - sha256: c86987475f162fadff579e7320c7ddda04cd2fdeffbe1129227a85d9ac9e03da - url: "https://pub.dev" - source: hosted - version: "1.1.11+1" - vector_graphics_compiler: - dependency: transitive - description: - name: vector_graphics_compiler - sha256: "12faff3f73b1741a36ca7e31b292ddeb629af819ca9efe9953b70bd63fc8cd81" - url: "https://pub.dev" - source: hosted - version: "1.1.11+1" - vector_math: - dependency: transitive - description: - name: vector_math - sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" - url: "https://pub.dev" - source: hosted - version: "2.1.4" - vm_service: - dependency: transitive - description: - name: vm_service - sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b - url: "https://pub.dev" - source: hosted - version: "14.3.0" - web: - dependency: transitive - description: - name: web - sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb - url: "https://pub.dev" - source: hosted - version: "1.1.0" - win32: - dependency: transitive - description: - name: win32 - sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 - url: "https://pub.dev" - source: hosted - version: "5.5.1" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d - url: "https://pub.dev" - source: hosted - version: "1.0.4" - xml: - dependency: transitive - description: - name: xml - sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 - url: "https://pub.dev" - source: hosted - version: "6.5.0" -sdks: - dart: ">=3.6.0 <4.0.0" - flutter: ">=3.27.0" diff --git a/pubspec.yaml b/pubspec.yaml index 7decc506..c4692ac4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -61,18 +61,15 @@ dependencies: firebase_crashlytics: ^4.3.2 firebase_database: ^11.3.2 bloc: ^9.0.0 + geocoding: ^4.0.0 + gauge_indicator: ^0.4.3 dev_dependencies: flutter_test: sdk: flutter - # The "flutter_lints" package below contains a set of recommended lints to - # encourage good coding practices. The lint set provided by the package is - # activated in the `analysis_options.yaml` file located at the root of your - # package. See that file for information about deactivating specific lint - # rules and activating additional ones. - flutter_lints: ^5.0.0 + very_good_analysis: ^9.0.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/windows/.gitignore b/windows/.gitignore deleted file mode 100644 index d492d0d9..00000000 --- a/windows/.gitignore +++ /dev/null @@ -1,17 +0,0 @@ -flutter/ephemeral/ - -# Visual Studio user-specific files. -*.suo -*.user -*.userosscache -*.sln.docstates - -# Visual Studio build-related files. -x64/ -x86/ - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!*.[Cc]ache/ diff --git a/windows/CMakeLists.txt b/windows/CMakeLists.txt deleted file mode 100644 index 05470a06..00000000 --- a/windows/CMakeLists.txt +++ /dev/null @@ -1,108 +0,0 @@ -# Project-level configuration. -cmake_minimum_required(VERSION 3.14) -project(syncrow_web LANGUAGES CXX) - -# The name of the executable created for the application. Change this to change -# the on-disk name of your application. -set(BINARY_NAME "syncrow_web") - -# Explicitly opt in to modern CMake behaviors to avoid warnings with recent -# versions of CMake. -cmake_policy(VERSION 3.14...3.25) - -# Define build configuration option. -get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) -if(IS_MULTICONFIG) - set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" - CACHE STRING "" FORCE) -else() - if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) - set(CMAKE_BUILD_TYPE "Debug" CACHE - STRING "Flutter build mode" FORCE) - set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS - "Debug" "Profile" "Release") - endif() -endif() -# Define settings for the Profile build mode. -set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") -set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") -set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") -set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") - -# Use Unicode for all projects. -add_definitions(-DUNICODE -D_UNICODE) - -# Compilation settings that should be applied to most targets. -# -# Be cautious about adding new options here, as plugins use this function by -# default. In most cases, you should add new options to specific targets instead -# of modifying this function. -function(APPLY_STANDARD_SETTINGS TARGET) - target_compile_features(${TARGET} PUBLIC cxx_std_17) - target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") - target_compile_options(${TARGET} PRIVATE /EHsc) - target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") - target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") -endfunction() - -# Flutter library and tool build rules. -set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") -add_subdirectory(${FLUTTER_MANAGED_DIR}) - -# Application build; see runner/CMakeLists.txt. -add_subdirectory("runner") - - -# Generated plugin build rules, which manage building the plugins and adding -# them to the application. -include(flutter/generated_plugins.cmake) - - -# === Installation === -# Support files are copied into place next to the executable, so that it can -# run in place. This is done instead of making a separate bundle (as on Linux) -# so that building and running from within Visual Studio will work. -set(BUILD_BUNDLE_DIR "$") -# Make the "install" step default, as it's required to run. -set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) -if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) - set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) -endif() - -set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") -set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") - -install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - COMPONENT Runtime) - -install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -if(PLUGIN_BUNDLED_LIBRARIES) - install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) -endif() - -# Copy the native assets provided by the build.dart from all packages. -set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") -install(DIRECTORY "${NATIVE_ASSETS_DIR}" - DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" - COMPONENT Runtime) - -# Fully re-copy the assets directory on each build to avoid having stale files -# from a previous install. -set(FLUTTER_ASSET_DIR_NAME "flutter_assets") -install(CODE " - file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") - " COMPONENT Runtime) -install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" - DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) - -# Install the AOT library on non-Debug builds only. -install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" - CONFIGURATIONS Profile;Release - COMPONENT Runtime) diff --git a/windows/flutter/CMakeLists.txt b/windows/flutter/CMakeLists.txt deleted file mode 100644 index 903f4899..00000000 --- a/windows/flutter/CMakeLists.txt +++ /dev/null @@ -1,109 +0,0 @@ -# This file controls Flutter-level build steps. It should not be edited. -cmake_minimum_required(VERSION 3.14) - -set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") - -# Configuration provided via flutter tool. -include(${EPHEMERAL_DIR}/generated_config.cmake) - -# TODO: Move the rest of this into files in ephemeral. See -# https://github.com/flutter/flutter/issues/57146. -set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") - -# Set fallback configurations for older versions of the flutter tool. -if (NOT DEFINED FLUTTER_TARGET_PLATFORM) - set(FLUTTER_TARGET_PLATFORM "windows-x64") -endif() - -# === Flutter Library === -set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") - -# Published to parent scope for install step. -set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) -set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) -set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) -set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) - -list(APPEND FLUTTER_LIBRARY_HEADERS - "flutter_export.h" - "flutter_windows.h" - "flutter_messenger.h" - "flutter_plugin_registrar.h" - "flutter_texture_registrar.h" -) -list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") -add_library(flutter INTERFACE) -target_include_directories(flutter INTERFACE - "${EPHEMERAL_DIR}" -) -target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") -add_dependencies(flutter flutter_assemble) - -# === Wrapper === -list(APPEND CPP_WRAPPER_SOURCES_CORE - "core_implementations.cc" - "standard_codec.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_PLUGIN - "plugin_registrar.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") -list(APPEND CPP_WRAPPER_SOURCES_APP - "flutter_engine.cc" - "flutter_view_controller.cc" -) -list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") - -# Wrapper sources needed for a plugin. -add_library(flutter_wrapper_plugin STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} -) -apply_standard_settings(flutter_wrapper_plugin) -set_target_properties(flutter_wrapper_plugin PROPERTIES - POSITION_INDEPENDENT_CODE ON) -set_target_properties(flutter_wrapper_plugin PROPERTIES - CXX_VISIBILITY_PRESET hidden) -target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) -target_include_directories(flutter_wrapper_plugin PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_plugin flutter_assemble) - -# Wrapper sources needed for the runner. -add_library(flutter_wrapper_app STATIC - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_APP} -) -apply_standard_settings(flutter_wrapper_app) -target_link_libraries(flutter_wrapper_app PUBLIC flutter) -target_include_directories(flutter_wrapper_app PUBLIC - "${WRAPPER_ROOT}/include" -) -add_dependencies(flutter_wrapper_app flutter_assemble) - -# === Flutter tool backend === -# _phony_ is a non-existent file to force this command to run every time, -# since currently there's no way to get a full input/output list from the -# flutter tool. -set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") -set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) -add_custom_command( - OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} - ${PHONY_OUTPUT} - COMMAND ${CMAKE_COMMAND} -E env - ${FLUTTER_TOOL_ENVIRONMENT} - "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" - ${FLUTTER_TARGET_PLATFORM} $ - VERBATIM -) -add_custom_target(flutter_assemble DEPENDS - "${FLUTTER_LIBRARY}" - ${FLUTTER_LIBRARY_HEADERS} - ${CPP_WRAPPER_SOURCES_CORE} - ${CPP_WRAPPER_SOURCES_PLUGIN} - ${CPP_WRAPPER_SOURCES_APP} -) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc deleted file mode 100644 index 0e0afee0..00000000 --- a/windows/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,20 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - -#include -#include -#include - -void RegisterPlugins(flutter::PluginRegistry* registry) { - FirebaseCorePluginCApiRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); - FlutterSecureStorageWindowsPluginRegisterWithRegistrar( - registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); - UrlLauncherWindowsRegisterWithRegistrar( - registry->GetRegistrarForPlugin("UrlLauncherWindows")); -} diff --git a/windows/flutter/generated_plugin_registrant.h b/windows/flutter/generated_plugin_registrant.h deleted file mode 100644 index dc139d85..00000000 --- a/windows/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void RegisterPlugins(flutter::PluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake deleted file mode 100644 index 9efea82a..00000000 --- a/windows/flutter/generated_plugins.cmake +++ /dev/null @@ -1,26 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST - firebase_core - flutter_secure_storage_windows - url_launcher_windows -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt deleted file mode 100644 index 394917c0..00000000 --- a/windows/runner/CMakeLists.txt +++ /dev/null @@ -1,40 +0,0 @@ -cmake_minimum_required(VERSION 3.14) -project(runner LANGUAGES CXX) - -# Define the application target. To change its name, change BINARY_NAME in the -# top-level CMakeLists.txt, not the value here, or `flutter run` will no longer -# work. -# -# Any new source files that you add to the application should be added here. -add_executable(${BINARY_NAME} WIN32 - "flutter_window.cpp" - "main.cpp" - "utils.cpp" - "win32_window.cpp" - "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" - "Runner.rc" - "runner.exe.manifest" -) - -# Apply the standard set of build settings. This can be removed for applications -# that need different build settings. -apply_standard_settings(${BINARY_NAME}) - -# Add preprocessor definitions for the build version. -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") -target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") - -# Disable Windows macros that collide with C++ standard library functions. -target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") - -# Add dependency libraries and include directories. Add any application-specific -# dependencies here. -target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) -target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") -target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") - -# Run the Flutter tool portions of the build. This must not be removed. -add_dependencies(${BINARY_NAME} flutter_assemble) diff --git a/windows/runner/Runner.rc b/windows/runner/Runner.rc deleted file mode 100644 index fd19641d..00000000 --- a/windows/runner/Runner.rc +++ /dev/null @@ -1,121 +0,0 @@ -// Microsoft Visual C++ generated resource script. -// -#pragma code_page(65001) -#include "resource.h" - -#define APSTUDIO_READONLY_SYMBOLS -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 2 resource. -// -#include "winres.h" - -///////////////////////////////////////////////////////////////////////////// -#undef APSTUDIO_READONLY_SYMBOLS - -///////////////////////////////////////////////////////////////////////////// -// English (United States) resources - -#if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) -LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US - -#ifdef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// TEXTINCLUDE -// - -1 TEXTINCLUDE -BEGIN - "resource.h\0" -END - -2 TEXTINCLUDE -BEGIN - "#include ""winres.h""\r\n" - "\0" -END - -3 TEXTINCLUDE -BEGIN - "\r\n" - "\0" -END - -#endif // APSTUDIO_INVOKED - - -///////////////////////////////////////////////////////////////////////////// -// -// Icon -// - -// Icon with lowest ID value placed first to ensure application icon -// remains consistent on all systems. -IDI_APP_ICON ICON "resources\\app_icon.ico" - - -///////////////////////////////////////////////////////////////////////////// -// -// Version -// - -#if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) -#define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD -#else -#define VERSION_AS_NUMBER 1,0,0,0 -#endif - -#if defined(FLUTTER_VERSION) -#define VERSION_AS_STRING FLUTTER_VERSION -#else -#define VERSION_AS_STRING "1.0.0" -#endif - -VS_VERSION_INFO VERSIONINFO - FILEVERSION VERSION_AS_NUMBER - PRODUCTVERSION VERSION_AS_NUMBER - FILEFLAGSMASK VS_FFI_FILEFLAGSMASK -#ifdef _DEBUG - FILEFLAGS VS_FF_DEBUG -#else - FILEFLAGS 0x0L -#endif - FILEOS VOS__WINDOWS32 - FILETYPE VFT_APP - FILESUBTYPE 0x0L -BEGIN - BLOCK "StringFileInfo" - BEGIN - BLOCK "040904e4" - BEGIN - VALUE "CompanyName", "com.example" "\0" - VALUE "FileDescription", "syncrow_web" "\0" - VALUE "FileVersion", VERSION_AS_STRING "\0" - VALUE "InternalName", "syncrow_web" "\0" - VALUE "LegalCopyright", "Copyright (C) 2024 com.example. All rights reserved." "\0" - VALUE "OriginalFilename", "syncrow_web.exe" "\0" - VALUE "ProductName", "syncrow_web" "\0" - VALUE "ProductVersion", VERSION_AS_STRING "\0" - END - END - BLOCK "VarFileInfo" - BEGIN - VALUE "Translation", 0x409, 1252 - END -END - -#endif // English (United States) resources -///////////////////////////////////////////////////////////////////////////// - - - -#ifndef APSTUDIO_INVOKED -///////////////////////////////////////////////////////////////////////////// -// -// Generated from the TEXTINCLUDE 3 resource. -// - - -///////////////////////////////////////////////////////////////////////////// -#endif // not APSTUDIO_INVOKED diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp deleted file mode 100644 index 955ee303..00000000 --- a/windows/runner/flutter_window.cpp +++ /dev/null @@ -1,71 +0,0 @@ -#include "flutter_window.h" - -#include - -#include "flutter/generated_plugin_registrant.h" - -FlutterWindow::FlutterWindow(const flutter::DartProject& project) - : project_(project) {} - -FlutterWindow::~FlutterWindow() {} - -bool FlutterWindow::OnCreate() { - if (!Win32Window::OnCreate()) { - return false; - } - - RECT frame = GetClientArea(); - - // The size here must match the window dimensions to avoid unnecessary surface - // creation / destruction in the startup path. - flutter_controller_ = std::make_unique( - frame.right - frame.left, frame.bottom - frame.top, project_); - // Ensure that basic setup of the controller was successful. - if (!flutter_controller_->engine() || !flutter_controller_->view()) { - return false; - } - RegisterPlugins(flutter_controller_->engine()); - SetChildContent(flutter_controller_->view()->GetNativeWindow()); - - flutter_controller_->engine()->SetNextFrameCallback([&]() { - this->Show(); - }); - - // Flutter can complete the first frame before the "show window" callback is - // registered. The following call ensures a frame is pending to ensure the - // window is shown. It is a no-op if the first frame hasn't completed yet. - flutter_controller_->ForceRedraw(); - - return true; -} - -void FlutterWindow::OnDestroy() { - if (flutter_controller_) { - flutter_controller_ = nullptr; - } - - Win32Window::OnDestroy(); -} - -LRESULT -FlutterWindow::MessageHandler(HWND hwnd, UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - // Give Flutter, including plugins, an opportunity to handle window messages. - if (flutter_controller_) { - std::optional result = - flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, - lparam); - if (result) { - return *result; - } - } - - switch (message) { - case WM_FONTCHANGE: - flutter_controller_->engine()->ReloadSystemFonts(); - break; - } - - return Win32Window::MessageHandler(hwnd, message, wparam, lparam); -} diff --git a/windows/runner/flutter_window.h b/windows/runner/flutter_window.h deleted file mode 100644 index 6da0652f..00000000 --- a/windows/runner/flutter_window.h +++ /dev/null @@ -1,33 +0,0 @@ -#ifndef RUNNER_FLUTTER_WINDOW_H_ -#define RUNNER_FLUTTER_WINDOW_H_ - -#include -#include - -#include - -#include "win32_window.h" - -// A window that does nothing but host a Flutter view. -class FlutterWindow : public Win32Window { - public: - // Creates a new FlutterWindow hosting a Flutter view running |project|. - explicit FlutterWindow(const flutter::DartProject& project); - virtual ~FlutterWindow(); - - protected: - // Win32Window: - bool OnCreate() override; - void OnDestroy() override; - LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, - LPARAM const lparam) noexcept override; - - private: - // The project to run. - flutter::DartProject project_; - - // The Flutter instance hosted by this window. - std::unique_ptr flutter_controller_; -}; - -#endif // RUNNER_FLUTTER_WINDOW_H_ diff --git a/windows/runner/main.cpp b/windows/runner/main.cpp deleted file mode 100644 index 4cb652fd..00000000 --- a/windows/runner/main.cpp +++ /dev/null @@ -1,43 +0,0 @@ -#include -#include -#include - -#include "flutter_window.h" -#include "utils.h" - -int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, - _In_ wchar_t *command_line, _In_ int show_command) { - // Attach to console when present (e.g., 'flutter run') or create a - // new console when running with a debugger. - if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { - CreateAndAttachConsole(); - } - - // Initialize COM, so that it is available for use in the library and/or - // plugins. - ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); - - flutter::DartProject project(L"data"); - - std::vector command_line_arguments = - GetCommandLineArguments(); - - project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); - - FlutterWindow window(project); - Win32Window::Point origin(10, 10); - Win32Window::Size size(1280, 720); - if (!window.Create(L"syncrow_web", origin, size)) { - return EXIT_FAILURE; - } - window.SetQuitOnClose(true); - - ::MSG msg; - while (::GetMessage(&msg, nullptr, 0, 0)) { - ::TranslateMessage(&msg); - ::DispatchMessage(&msg); - } - - ::CoUninitialize(); - return EXIT_SUCCESS; -} diff --git a/windows/runner/resource.h b/windows/runner/resource.h deleted file mode 100644 index 66a65d1e..00000000 --- a/windows/runner/resource.h +++ /dev/null @@ -1,16 +0,0 @@ -//{{NO_DEPENDENCIES}} -// Microsoft Visual C++ generated include file. -// Used by Runner.rc -// -#define IDI_APP_ICON 101 - -// Next default values for new objects -// -#ifdef APSTUDIO_INVOKED -#ifndef APSTUDIO_READONLY_SYMBOLS -#define _APS_NEXT_RESOURCE_VALUE 102 -#define _APS_NEXT_COMMAND_VALUE 40001 -#define _APS_NEXT_CONTROL_VALUE 1001 -#define _APS_NEXT_SYMED_VALUE 101 -#endif -#endif diff --git a/windows/runner/resources/app_icon.ico b/windows/runner/resources/app_icon.ico deleted file mode 100644 index c04e20ca..00000000 Binary files a/windows/runner/resources/app_icon.ico and /dev/null differ diff --git a/windows/runner/runner.exe.manifest b/windows/runner/runner.exe.manifest deleted file mode 100644 index a42ea768..00000000 --- a/windows/runner/runner.exe.manifest +++ /dev/null @@ -1,20 +0,0 @@ - - - - - PerMonitorV2 - - - - - - - - - - - - - - - diff --git a/windows/runner/utils.cpp b/windows/runner/utils.cpp deleted file mode 100644 index b2b08734..00000000 --- a/windows/runner/utils.cpp +++ /dev/null @@ -1,65 +0,0 @@ -#include "utils.h" - -#include -#include -#include -#include - -#include - -void CreateAndAttachConsole() { - if (::AllocConsole()) { - FILE *unused; - if (freopen_s(&unused, "CONOUT$", "w", stdout)) { - _dup2(_fileno(stdout), 1); - } - if (freopen_s(&unused, "CONOUT$", "w", stderr)) { - _dup2(_fileno(stdout), 2); - } - std::ios::sync_with_stdio(); - FlutterDesktopResyncOutputStreams(); - } -} - -std::vector GetCommandLineArguments() { - // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. - int argc; - wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); - if (argv == nullptr) { - return std::vector(); - } - - std::vector command_line_arguments; - - // Skip the first argument as it's the binary name. - for (int i = 1; i < argc; i++) { - command_line_arguments.push_back(Utf8FromUtf16(argv[i])); - } - - ::LocalFree(argv); - - return command_line_arguments; -} - -std::string Utf8FromUtf16(const wchar_t* utf16_string) { - if (utf16_string == nullptr) { - return std::string(); - } - int target_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - -1, nullptr, 0, nullptr, nullptr) - -1; // remove the trailing null character - int input_length = (int)wcslen(utf16_string); - std::string utf8_string; - if (target_length <= 0 || target_length > utf8_string.max_size()) { - return utf8_string; - } - utf8_string.resize(target_length); - int converted_length = ::WideCharToMultiByte( - CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, - input_length, utf8_string.data(), target_length, nullptr, nullptr); - if (converted_length == 0) { - return std::string(); - } - return utf8_string; -} diff --git a/windows/runner/utils.h b/windows/runner/utils.h deleted file mode 100644 index 3879d547..00000000 --- a/windows/runner/utils.h +++ /dev/null @@ -1,19 +0,0 @@ -#ifndef RUNNER_UTILS_H_ -#define RUNNER_UTILS_H_ - -#include -#include - -// Creates a console for the process, and redirects stdout and stderr to -// it for both the runner and the Flutter library. -void CreateAndAttachConsole(); - -// Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string -// encoded in UTF-8. Returns an empty std::string on failure. -std::string Utf8FromUtf16(const wchar_t* utf16_string); - -// Gets the command line arguments passed in as a std::vector, -// encoded in UTF-8. Returns an empty std::vector on failure. -std::vector GetCommandLineArguments(); - -#endif // RUNNER_UTILS_H_ diff --git a/windows/runner/win32_window.cpp b/windows/runner/win32_window.cpp deleted file mode 100644 index 60608d0f..00000000 --- a/windows/runner/win32_window.cpp +++ /dev/null @@ -1,288 +0,0 @@ -#include "win32_window.h" - -#include -#include - -#include "resource.h" - -namespace { - -/// Window attribute that enables dark mode window decorations. -/// -/// Redefined in case the developer's machine has a Windows SDK older than -/// version 10.0.22000.0. -/// See: https://docs.microsoft.com/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute -#ifndef DWMWA_USE_IMMERSIVE_DARK_MODE -#define DWMWA_USE_IMMERSIVE_DARK_MODE 20 -#endif - -constexpr const wchar_t kWindowClassName[] = L"FLUTTER_RUNNER_WIN32_WINDOW"; - -/// Registry key for app theme preference. -/// -/// A value of 0 indicates apps should use dark mode. A non-zero or missing -/// value indicates apps should use light mode. -constexpr const wchar_t kGetPreferredBrightnessRegKey[] = - L"Software\\Microsoft\\Windows\\CurrentVersion\\Themes\\Personalize"; -constexpr const wchar_t kGetPreferredBrightnessRegValue[] = L"AppsUseLightTheme"; - -// The number of Win32Window objects that currently exist. -static int g_active_window_count = 0; - -using EnableNonClientDpiScaling = BOOL __stdcall(HWND hwnd); - -// Scale helper to convert logical scaler values to physical using passed in -// scale factor -int Scale(int source, double scale_factor) { - return static_cast(source * scale_factor); -} - -// Dynamically loads the |EnableNonClientDpiScaling| from the User32 module. -// This API is only needed for PerMonitor V1 awareness mode. -void EnableFullDpiSupportIfAvailable(HWND hwnd) { - HMODULE user32_module = LoadLibraryA("User32.dll"); - if (!user32_module) { - return; - } - auto enable_non_client_dpi_scaling = - reinterpret_cast( - GetProcAddress(user32_module, "EnableNonClientDpiScaling")); - if (enable_non_client_dpi_scaling != nullptr) { - enable_non_client_dpi_scaling(hwnd); - } - FreeLibrary(user32_module); -} - -} // namespace - -// Manages the Win32Window's window class registration. -class WindowClassRegistrar { - public: - ~WindowClassRegistrar() = default; - - // Returns the singleton registrar instance. - static WindowClassRegistrar* GetInstance() { - if (!instance_) { - instance_ = new WindowClassRegistrar(); - } - return instance_; - } - - // Returns the name of the window class, registering the class if it hasn't - // previously been registered. - const wchar_t* GetWindowClass(); - - // Unregisters the window class. Should only be called if there are no - // instances of the window. - void UnregisterWindowClass(); - - private: - WindowClassRegistrar() = default; - - static WindowClassRegistrar* instance_; - - bool class_registered_ = false; -}; - -WindowClassRegistrar* WindowClassRegistrar::instance_ = nullptr; - -const wchar_t* WindowClassRegistrar::GetWindowClass() { - if (!class_registered_) { - WNDCLASS window_class{}; - window_class.hCursor = LoadCursor(nullptr, IDC_ARROW); - window_class.lpszClassName = kWindowClassName; - window_class.style = CS_HREDRAW | CS_VREDRAW; - window_class.cbClsExtra = 0; - window_class.cbWndExtra = 0; - window_class.hInstance = GetModuleHandle(nullptr); - window_class.hIcon = - LoadIcon(window_class.hInstance, MAKEINTRESOURCE(IDI_APP_ICON)); - window_class.hbrBackground = 0; - window_class.lpszMenuName = nullptr; - window_class.lpfnWndProc = Win32Window::WndProc; - RegisterClass(&window_class); - class_registered_ = true; - } - return kWindowClassName; -} - -void WindowClassRegistrar::UnregisterWindowClass() { - UnregisterClass(kWindowClassName, nullptr); - class_registered_ = false; -} - -Win32Window::Win32Window() { - ++g_active_window_count; -} - -Win32Window::~Win32Window() { - --g_active_window_count; - Destroy(); -} - -bool Win32Window::Create(const std::wstring& title, - const Point& origin, - const Size& size) { - Destroy(); - - const wchar_t* window_class = - WindowClassRegistrar::GetInstance()->GetWindowClass(); - - const POINT target_point = {static_cast(origin.x), - static_cast(origin.y)}; - HMONITOR monitor = MonitorFromPoint(target_point, MONITOR_DEFAULTTONEAREST); - UINT dpi = FlutterDesktopGetDpiForMonitor(monitor); - double scale_factor = dpi / 96.0; - - HWND window = CreateWindow( - window_class, title.c_str(), WS_OVERLAPPEDWINDOW, - Scale(origin.x, scale_factor), Scale(origin.y, scale_factor), - Scale(size.width, scale_factor), Scale(size.height, scale_factor), - nullptr, nullptr, GetModuleHandle(nullptr), this); - - if (!window) { - return false; - } - - UpdateTheme(window); - - return OnCreate(); -} - -bool Win32Window::Show() { - return ShowWindow(window_handle_, SW_SHOWNORMAL); -} - -// static -LRESULT CALLBACK Win32Window::WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - if (message == WM_NCCREATE) { - auto window_struct = reinterpret_cast(lparam); - SetWindowLongPtr(window, GWLP_USERDATA, - reinterpret_cast(window_struct->lpCreateParams)); - - auto that = static_cast(window_struct->lpCreateParams); - EnableFullDpiSupportIfAvailable(window); - that->window_handle_ = window; - } else if (Win32Window* that = GetThisFromHandle(window)) { - return that->MessageHandler(window, message, wparam, lparam); - } - - return DefWindowProc(window, message, wparam, lparam); -} - -LRESULT -Win32Window::MessageHandler(HWND hwnd, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept { - switch (message) { - case WM_DESTROY: - window_handle_ = nullptr; - Destroy(); - if (quit_on_close_) { - PostQuitMessage(0); - } - return 0; - - case WM_DPICHANGED: { - auto newRectSize = reinterpret_cast(lparam); - LONG newWidth = newRectSize->right - newRectSize->left; - LONG newHeight = newRectSize->bottom - newRectSize->top; - - SetWindowPos(hwnd, nullptr, newRectSize->left, newRectSize->top, newWidth, - newHeight, SWP_NOZORDER | SWP_NOACTIVATE); - - return 0; - } - case WM_SIZE: { - RECT rect = GetClientArea(); - if (child_content_ != nullptr) { - // Size and position the child window. - MoveWindow(child_content_, rect.left, rect.top, rect.right - rect.left, - rect.bottom - rect.top, TRUE); - } - return 0; - } - - case WM_ACTIVATE: - if (child_content_ != nullptr) { - SetFocus(child_content_); - } - return 0; - - case WM_DWMCOLORIZATIONCOLORCHANGED: - UpdateTheme(hwnd); - return 0; - } - - return DefWindowProc(window_handle_, message, wparam, lparam); -} - -void Win32Window::Destroy() { - OnDestroy(); - - if (window_handle_) { - DestroyWindow(window_handle_); - window_handle_ = nullptr; - } - if (g_active_window_count == 0) { - WindowClassRegistrar::GetInstance()->UnregisterWindowClass(); - } -} - -Win32Window* Win32Window::GetThisFromHandle(HWND const window) noexcept { - return reinterpret_cast( - GetWindowLongPtr(window, GWLP_USERDATA)); -} - -void Win32Window::SetChildContent(HWND content) { - child_content_ = content; - SetParent(content, window_handle_); - RECT frame = GetClientArea(); - - MoveWindow(content, frame.left, frame.top, frame.right - frame.left, - frame.bottom - frame.top, true); - - SetFocus(child_content_); -} - -RECT Win32Window::GetClientArea() { - RECT frame; - GetClientRect(window_handle_, &frame); - return frame; -} - -HWND Win32Window::GetHandle() { - return window_handle_; -} - -void Win32Window::SetQuitOnClose(bool quit_on_close) { - quit_on_close_ = quit_on_close; -} - -bool Win32Window::OnCreate() { - // No-op; provided for subclasses. - return true; -} - -void Win32Window::OnDestroy() { - // No-op; provided for subclasses. -} - -void Win32Window::UpdateTheme(HWND const window) { - DWORD light_mode; - DWORD light_mode_size = sizeof(light_mode); - LSTATUS result = RegGetValue(HKEY_CURRENT_USER, kGetPreferredBrightnessRegKey, - kGetPreferredBrightnessRegValue, - RRF_RT_REG_DWORD, nullptr, &light_mode, - &light_mode_size); - - if (result == ERROR_SUCCESS) { - BOOL enable_dark_mode = light_mode == 0; - DwmSetWindowAttribute(window, DWMWA_USE_IMMERSIVE_DARK_MODE, - &enable_dark_mode, sizeof(enable_dark_mode)); - } -} diff --git a/windows/runner/win32_window.h b/windows/runner/win32_window.h deleted file mode 100644 index e901dde6..00000000 --- a/windows/runner/win32_window.h +++ /dev/null @@ -1,102 +0,0 @@ -#ifndef RUNNER_WIN32_WINDOW_H_ -#define RUNNER_WIN32_WINDOW_H_ - -#include - -#include -#include -#include - -// A class abstraction for a high DPI-aware Win32 Window. Intended to be -// inherited from by classes that wish to specialize with custom -// rendering and input handling -class Win32Window { - public: - struct Point { - unsigned int x; - unsigned int y; - Point(unsigned int x, unsigned int y) : x(x), y(y) {} - }; - - struct Size { - unsigned int width; - unsigned int height; - Size(unsigned int width, unsigned int height) - : width(width), height(height) {} - }; - - Win32Window(); - virtual ~Win32Window(); - - // Creates a win32 window with |title| that is positioned and sized using - // |origin| and |size|. New windows are created on the default monitor. Window - // sizes are specified to the OS in physical pixels, hence to ensure a - // consistent size this function will scale the inputted width and height as - // as appropriate for the default monitor. The window is invisible until - // |Show| is called. Returns true if the window was created successfully. - bool Create(const std::wstring& title, const Point& origin, const Size& size); - - // Show the current window. Returns true if the window was successfully shown. - bool Show(); - - // Release OS resources associated with window. - void Destroy(); - - // Inserts |content| into the window tree. - void SetChildContent(HWND content); - - // Returns the backing Window handle to enable clients to set icon and other - // window properties. Returns nullptr if the window has been destroyed. - HWND GetHandle(); - - // If true, closing this window will quit the application. - void SetQuitOnClose(bool quit_on_close); - - // Return a RECT representing the bounds of the current client area. - RECT GetClientArea(); - - protected: - // Processes and route salient window messages for mouse handling, - // size change and DPI. Delegates handling of these to member overloads that - // inheriting classes can handle. - virtual LRESULT MessageHandler(HWND window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Called when CreateAndShow is called, allowing subclass window-related - // setup. Subclasses should return false if setup fails. - virtual bool OnCreate(); - - // Called when Destroy is called. - virtual void OnDestroy(); - - private: - friend class WindowClassRegistrar; - - // OS callback called by message pump. Handles the WM_NCCREATE message which - // is passed when the non-client area is being created and enables automatic - // non-client DPI scaling so that the non-client area automatically - // responds to changes in DPI. All other messages are handled by - // MessageHandler. - static LRESULT CALLBACK WndProc(HWND const window, - UINT const message, - WPARAM const wparam, - LPARAM const lparam) noexcept; - - // Retrieves a class instance pointer for |window| - static Win32Window* GetThisFromHandle(HWND const window) noexcept; - - // Update the window frame's theme to match the system theme. - static void UpdateTheme(HWND const window); - - bool quit_on_close_ = false; - - // window handle for top level window. - HWND window_handle_ = nullptr; - - // window handle for hosted content. - HWND child_content_ = nullptr; -}; - -#endif // RUNNER_WIN32_WINDOW_H_