Test pre-commit hook
45
.gitattributes
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Normalize all text files to LF line endings
|
||||||
|
* text=auto eol=lf
|
||||||
|
|
||||||
|
# Treat Dart, YAML, XML, Swift, and other source files as text
|
||||||
|
*.dart text
|
||||||
|
*.yaml text
|
||||||
|
*.yml text
|
||||||
|
*.xml text
|
||||||
|
*.swift text
|
||||||
|
*.java text
|
||||||
|
*.kt text
|
||||||
|
*.gradle text
|
||||||
|
*.properties text
|
||||||
|
*.json text
|
||||||
|
*.html text
|
||||||
|
*.css text
|
||||||
|
*.js text
|
||||||
|
*.ts text
|
||||||
|
*.md text
|
||||||
|
*.txt text
|
||||||
|
|
||||||
|
# Flutter platform-specific files
|
||||||
|
*.pbxproj text
|
||||||
|
*.xcconfig text
|
||||||
|
*.cmake text
|
||||||
|
*.cc text
|
||||||
|
*.h text
|
||||||
|
|
||||||
|
# Binary files (no line ending normalization)
|
||||||
|
*.png binary
|
||||||
|
*.jpg binary
|
||||||
|
*.jpeg binary
|
||||||
|
*.gif binary
|
||||||
|
*.ico binary
|
||||||
|
*.pdf binary
|
||||||
|
*.ttf binary
|
||||||
|
*.otf binary
|
||||||
|
*.mp3 binary
|
||||||
|
*.mp4 binary
|
||||||
|
*.zip binary
|
||||||
|
|
||||||
|
# Lock files and build artifacts
|
||||||
|
pubspec.lock text
|
||||||
|
*.lock text
|
||||||
|
*.log text
|
||||||
45
.gitignore
vendored
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# Miscellaneous
|
||||||
|
*.class
|
||||||
|
*.log
|
||||||
|
*.pyc
|
||||||
|
*.swp
|
||||||
|
.DS_Store
|
||||||
|
.atom/
|
||||||
|
.build/
|
||||||
|
.buildlog/
|
||||||
|
.history
|
||||||
|
.svn/
|
||||||
|
.swiftpm/
|
||||||
|
migrate_working_dir/
|
||||||
|
|
||||||
|
# IntelliJ related
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
.idea/
|
||||||
|
|
||||||
|
# The .vscode folder contains launch configuration and tasks you configure in
|
||||||
|
# VS Code which you may wish to be included in version control, so this line
|
||||||
|
# is commented out by default.
|
||||||
|
#.vscode/
|
||||||
|
|
||||||
|
# Flutter/Dart/Pub related
|
||||||
|
**/doc/api/
|
||||||
|
**/ios/Flutter/.last_build_id
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
.pub-cache/
|
||||||
|
.pub/
|
||||||
|
/build/
|
||||||
|
|
||||||
|
# Symbolication related
|
||||||
|
app.*.symbols
|
||||||
|
|
||||||
|
# Obfuscation related
|
||||||
|
app.*.map.json
|
||||||
|
|
||||||
|
# Android Studio will place build artifacts here
|
||||||
|
/android/app/debug
|
||||||
|
/android/app/profile
|
||||||
|
/android/app/release
|
||||||
45
.metadata
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# This file tracks properties of this Flutter project.
|
||||||
|
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||||
|
#
|
||||||
|
# This file should be version controlled and should not be manually edited.
|
||||||
|
|
||||||
|
version:
|
||||||
|
revision: "b25305a8832cfc6ba632a7f87ad455e319dccce8"
|
||||||
|
channel: "stable"
|
||||||
|
|
||||||
|
project_type: app
|
||||||
|
|
||||||
|
# Tracks metadata for the flutter migrate command
|
||||||
|
migration:
|
||||||
|
platforms:
|
||||||
|
- platform: root
|
||||||
|
create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
- platform: android
|
||||||
|
create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
- platform: ios
|
||||||
|
create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
- platform: linux
|
||||||
|
create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
- platform: macos
|
||||||
|
create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
- platform: web
|
||||||
|
create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
- platform: windows
|
||||||
|
create_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
base_revision: b25305a8832cfc6ba632a7f87ad455e319dccce8
|
||||||
|
|
||||||
|
# User provided section
|
||||||
|
|
||||||
|
# List of Local paths (relative to this file) that should be
|
||||||
|
# ignored by the migrate tool.
|
||||||
|
#
|
||||||
|
# Files that are not part of the templates will be ignored by default.
|
||||||
|
unmanaged_files:
|
||||||
|
- 'lib/main.dart'
|
||||||
|
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||||
16
README.md
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
# environment_monitoring_app
|
||||||
|
|
||||||
|
A new Flutter project.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
This project is a starting point for a Flutter application.
|
||||||
|
|
||||||
|
A few resources to get you started if this is your first Flutter project:
|
||||||
|
|
||||||
|
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
|
||||||
|
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
|
||||||
|
|
||||||
|
For help getting started with Flutter development, view the
|
||||||
|
[online documentation](https://docs.flutter.dev/), which offers tutorials,
|
||||||
|
samples, guidance on mobile development, and a full API reference.
|
||||||
28
analysis_options.yaml
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
# This file configures the analyzer, which statically analyzes Dart code to
|
||||||
|
# check for errors, warnings, and lints.
|
||||||
|
#
|
||||||
|
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
|
||||||
|
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
|
||||||
|
# invoked from the command line by running `flutter analyze`.
|
||||||
|
|
||||||
|
# The following line activates a set of recommended lints for Flutter apps,
|
||||||
|
# packages, and plugins designed to encourage good coding practices.
|
||||||
|
include: package:flutter_lints/flutter.yaml
|
||||||
|
|
||||||
|
linter:
|
||||||
|
# The lint rules applied to this project can be customized in the
|
||||||
|
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
|
||||||
|
# included above or to enable additional rules. A list of all available lints
|
||||||
|
# and their documentation is published at https://dart.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
|
||||||
|
|
||||||
|
# Additional information about this file can be found at
|
||||||
|
# https://dart.dev/guides/language/analysis-options
|
||||||
14
android/.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
gradle-wrapper.jar
|
||||||
|
/.gradle
|
||||||
|
/captures/
|
||||||
|
/gradlew
|
||||||
|
/gradlew.bat
|
||||||
|
/local.properties
|
||||||
|
GeneratedPluginRegistrant.java
|
||||||
|
.cxx/
|
||||||
|
|
||||||
|
# Remember to never publicly share your keystore.
|
||||||
|
# See https://flutter.dev/to/reference-keystore
|
||||||
|
key.properties
|
||||||
|
**/*.keystore
|
||||||
|
**/*.jks
|
||||||
50
android/app/build.gradle.kts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
plugins {
|
||||||
|
id("com.android.application")
|
||||||
|
id("kotlin-android")
|
||||||
|
// Flutter plugin must be applied last
|
||||||
|
id("dev.flutter.flutter-gradle-plugin")
|
||||||
|
}
|
||||||
|
|
||||||
|
android {
|
||||||
|
namespace = "com.example.environment_monitoring_app"
|
||||||
|
compileSdk = flutter.compileSdkVersion
|
||||||
|
ndkVersion = "27.0.12077973"
|
||||||
|
|
||||||
|
compileOptions {
|
||||||
|
sourceCompatibility = JavaVersion.VERSION_11
|
||||||
|
targetCompatibility = JavaVersion.VERSION_11
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlinOptions {
|
||||||
|
jvmTarget = JavaVersion.VERSION_11.toString()
|
||||||
|
}
|
||||||
|
|
||||||
|
defaultConfig {
|
||||||
|
applicationId = "com.example.environment_monitoring_app"
|
||||||
|
minSdk = 21
|
||||||
|
targetSdk = 35
|
||||||
|
versionCode = 1
|
||||||
|
versionName = flutter.versionName
|
||||||
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
create("release") {
|
||||||
|
storeFile = file("upload-keystore.jks")
|
||||||
|
storePassword = "0nly1kn0wthep@5sw0rd"
|
||||||
|
keyAlias = "upload"
|
||||||
|
keyPassword = "0nly1kn0wthep@5sw0rd"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buildTypes {
|
||||||
|
getByName("release") {
|
||||||
|
signingConfig = signingConfigs.getByName("release")
|
||||||
|
isMinifyEnabled = false
|
||||||
|
isShrinkResources = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
flutter {
|
||||||
|
source = "../.."
|
||||||
|
}
|
||||||
7
android/app/src/debug/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
77
android/app/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- Permissions required for network connectivity -->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||||
|
|
||||||
|
<!-- Location Permissions (already present, required for Bluetooth scanning) -->
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||||
|
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||||
|
|
||||||
|
<!-- Camera Permission (for barcode scanning) -->
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
|
||||||
|
<!-- Bluetooth Permissions (for sonde connection) -->
|
||||||
|
<!-- Required for Android 12 and newer -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
|
||||||
|
<!-- Required for older Android versions -->
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH" />
|
||||||
|
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
|
||||||
|
|
||||||
|
<!-- USB Host Feature Declaration (for sonde connection) -->
|
||||||
|
<uses-feature android:name="android.hardware.usb.host" android:required="false" />
|
||||||
|
|
||||||
|
<!-- START: STORAGE PERMISSIONS -->
|
||||||
|
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
|
||||||
|
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
|
||||||
|
<!-- END: STORAGE PERMISSIONS -->
|
||||||
|
|
||||||
|
<application
|
||||||
|
android:label="MMS V4 Debug"
|
||||||
|
android:name="${applicationName}"
|
||||||
|
android:icon="@mipmap/ic_launcher"
|
||||||
|
android:requestLegacyExternalStorage="true">
|
||||||
|
<activity
|
||||||
|
android:name=".MainActivity"
|
||||||
|
android:exported="true"
|
||||||
|
android:launchMode="singleTop"
|
||||||
|
android:taskAffinity=""
|
||||||
|
android:theme="@style/LaunchTheme"
|
||||||
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
|
android:hardwareAccelerated="true"
|
||||||
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
|
</intent-filter>
|
||||||
|
|
||||||
|
<!-- Intent filter to detect when a USB device is attached -->
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
|
||||||
|
</intent-filter>
|
||||||
|
<!-- Meta-data to specify which USB devices the app can handle -->
|
||||||
|
<meta-data
|
||||||
|
android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED"
|
||||||
|
android:resource="@xml/device_filter" />
|
||||||
|
|
||||||
|
</activity>
|
||||||
|
|
||||||
|
<meta-data
|
||||||
|
android:name="flutterEmbedding"
|
||||||
|
android:value="2" />
|
||||||
|
|
||||||
|
</application>
|
||||||
|
|
||||||
|
<queries>
|
||||||
|
<intent>
|
||||||
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
|
<data android:mimeType="text/plain"/>
|
||||||
|
</intent>
|
||||||
|
</queries>
|
||||||
|
</manifest>
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
package com.example.environment_monitoring_app
|
||||||
|
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.hardware.usb.UsbDevice
|
||||||
|
import android.hardware.usb.UsbManager
|
||||||
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
import io.flutter.embedding.engine.FlutterEngine
|
||||||
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
|
class MainActivity: FlutterActivity() {
|
||||||
|
// This channel name must match the one used in your Dart code (e.g., in the InSituSamplingService).
|
||||||
|
private val CHANNEL = "com.example.environment_monitoring_app/usb"
|
||||||
|
private val ACTION_USB_PERMISSION = "com.example.environment_monitoring_app.USB_PERMISSION"
|
||||||
|
|
||||||
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
|
super.configureFlutterEngine(flutterEngine)
|
||||||
|
|
||||||
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
|
||||||
|
// This block listens for method calls from your Flutter app.
|
||||||
|
if (call.method == "requestUsbPermission") {
|
||||||
|
val manager = getSystemService(Context.USB_SERVICE) as UsbManager
|
||||||
|
var deviceToRequest: UsbDevice? = null
|
||||||
|
|
||||||
|
// Get the Vendor ID and Product ID sent from the Dart code.
|
||||||
|
val vid = call.argument<Int>("vid")
|
||||||
|
val pid = call.argument<Int>("pid")
|
||||||
|
|
||||||
|
// Find the matching USB device connected to the phone.
|
||||||
|
manager.deviceList.values.forEach { device ->
|
||||||
|
if (device.vendorId == vid && device.productId == pid) {
|
||||||
|
deviceToRequest = device
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceToRequest?.let {
|
||||||
|
// If the device is found, create an intent and request permission from the user.
|
||||||
|
// The 'usb_serial' package in Flutter will listen for the result of this permission request.
|
||||||
|
val usbPermissionIntent = PendingIntent.getBroadcast(this, 0, Intent(ACTION_USB_PERMISSION), PendingIntent.FLAG_IMMUTABLE)
|
||||||
|
manager.requestPermission(it, usbPermissionIntent)
|
||||||
|
result.success(true) // Inform Dart that the request was successfully sent.
|
||||||
|
} ?: run {
|
||||||
|
// If the device is not found, send an error back to Dart.
|
||||||
|
result.error("DEVICE_NOT_FOUND", "Specified USB device not found.", null)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result.notImplemented()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
12
android/app/src/main/res/drawable-v21/launch_background.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="?android:colorBackground" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
12
android/app/src/main/res/drawable/launch_background.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- Modify this file to customize your launch splash screen -->
|
||||||
|
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<item android:drawable="@android:color/white" />
|
||||||
|
|
||||||
|
<!-- You can insert your own image assets here -->
|
||||||
|
<!-- <item>
|
||||||
|
<bitmap
|
||||||
|
android:gravity="center"
|
||||||
|
android:src="@mipmap/launch_image" />
|
||||||
|
</item> -->
|
||||||
|
</layer-list>
|
||||||
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 3.6 KiB |
BIN
android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
18
android/app/src/main/res/values-night/styles.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is on -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
18
android/app/src/main/res/values/styles.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<!-- Theme applied to the Android Window while the process is starting when the OS's Dark Mode setting is off -->
|
||||||
|
<style name="LaunchTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<!-- Show a splash screen on the activity. Automatically removed when
|
||||||
|
the Flutter engine draws its first frame -->
|
||||||
|
<item name="android:windowBackground">@drawable/launch_background</item>
|
||||||
|
</style>
|
||||||
|
<!-- Theme applied to the Android Window as soon as the process has started.
|
||||||
|
This theme determines the color of the Android Window while your
|
||||||
|
Flutter UI initializes, as well as behind your Flutter UI while its
|
||||||
|
running.
|
||||||
|
|
||||||
|
This Theme is only used starting with V2 of Flutter's Android embedding. -->
|
||||||
|
<style name="NormalTheme" parent="@android:style/Theme.Light.NoTitleBar">
|
||||||
|
<item name="android:windowBackground">?android:colorBackground</item>
|
||||||
|
</style>
|
||||||
|
</resources>
|
||||||
8
android/app/src/main/res/xml/device_filter.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<usb-device vendor-id="1659" product-id="9123" />
|
||||||
|
<usb-device vendor-id="4292" product-id="60000" /> <!-- CP2102: 10C4:EA60 -->
|
||||||
|
<usb-device vendor-id="6790" product-id="29987" /> <!-- usb to serial rs232-->
|
||||||
|
<usb-device vendor-id="4362" product-id="4432" />
|
||||||
|
<usb-device vendor-id="1659" product-id="8963" />
|
||||||
|
</resources>
|
||||||
7
android/app/src/profile/AndroidManifest.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
|
<!-- The INTERNET permission is required for development. Specifically,
|
||||||
|
the Flutter tool needs it to communicate with the running application
|
||||||
|
to allow setting breakpoints, to provide hot reload, etc.
|
||||||
|
-->
|
||||||
|
<uses-permission android:name="android.permission.INTERNET"/>
|
||||||
|
</manifest>
|
||||||
21
android/build.gradle.kts
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
allprojects {
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val newBuildDir: Directory = rootProject.layout.buildDirectory.dir("../../build").get()
|
||||||
|
rootProject.layout.buildDirectory.value(newBuildDir)
|
||||||
|
|
||||||
|
subprojects {
|
||||||
|
val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name)
|
||||||
|
project.layout.buildDirectory.value(newSubprojectBuildDir)
|
||||||
|
}
|
||||||
|
subprojects {
|
||||||
|
project.evaluationDependsOn(":app")
|
||||||
|
}
|
||||||
|
|
||||||
|
tasks.register<Delete>("clean") {
|
||||||
|
delete(rootProject.layout.buildDirectory)
|
||||||
|
}
|
||||||
3
android/gradle.properties
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError
|
||||||
|
android.useAndroidX=true
|
||||||
|
android.enableJetifier=true
|
||||||
5
android/gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
distributionBase=GRADLE_USER_HOME
|
||||||
|
distributionPath=wrapper/dists
|
||||||
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
zipStorePath=wrapper/dists
|
||||||
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip
|
||||||
25
android/settings.gradle.kts
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
pluginManagement {
|
||||||
|
val flutterSdkPath = run {
|
||||||
|
val properties = java.util.Properties()
|
||||||
|
file("local.properties").inputStream().use { properties.load(it) }
|
||||||
|
val flutterSdkPath = properties.getProperty("flutter.sdk")
|
||||||
|
require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" }
|
||||||
|
flutterSdkPath
|
||||||
|
}
|
||||||
|
|
||||||
|
includeBuild("$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 "8.7.3" apply false
|
||||||
|
id("org.jetbrains.kotlin.android") version "2.1.0" apply false
|
||||||
|
}
|
||||||
|
|
||||||
|
include(":app")
|
||||||
BIN
assets/icon2.png
Normal file
|
After Width: | Height: | Size: 5.7 KiB |
BIN
assets/icon3.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
assets/icon4.png
Normal file
|
After Width: | Height: | Size: 12 KiB |
BIN
assets/icon_1_512x512.png
Normal file
|
After Width: | Height: | Size: 274 KiB |
BIN
assets/icon_2_512x512.png
Normal file
|
After Width: | Height: | Size: 268 KiB |
BIN
assets/icon_3_512x512.png
Normal file
|
After Width: | Height: | Size: 269 KiB |
34
ios/.gitignore
vendored
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
**/dgph
|
||||||
|
*.mode1v3
|
||||||
|
*.mode2v3
|
||||||
|
*.moved-aside
|
||||||
|
*.pbxuser
|
||||||
|
*.perspectivev3
|
||||||
|
**/*sync/
|
||||||
|
.sconsign.dblite
|
||||||
|
.tags*
|
||||||
|
**/.vagrant/
|
||||||
|
**/DerivedData/
|
||||||
|
Icon?
|
||||||
|
**/Pods/
|
||||||
|
**/.symlinks/
|
||||||
|
profile
|
||||||
|
xcuserdata
|
||||||
|
**/.generated/
|
||||||
|
Flutter/App.framework
|
||||||
|
Flutter/Flutter.framework
|
||||||
|
Flutter/Flutter.podspec
|
||||||
|
Flutter/Generated.xcconfig
|
||||||
|
Flutter/ephemeral/
|
||||||
|
Flutter/app.flx
|
||||||
|
Flutter/app.zip
|
||||||
|
Flutter/flutter_assets/
|
||||||
|
Flutter/flutter_export_environment.sh
|
||||||
|
ServiceDefinitions.json
|
||||||
|
Runner/GeneratedPluginRegistrant.*
|
||||||
|
|
||||||
|
# Exceptions to above rules.
|
||||||
|
!default.mode1v3
|
||||||
|
!default.mode2v3
|
||||||
|
!default.pbxuser
|
||||||
|
!default.perspectivev3
|
||||||
26
ios/Flutter/AppFrameworkInfo.plist
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>en</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>io.flutter.flutter.app</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>App</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>FMWK</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>1.0</string>
|
||||||
|
<key>MinimumOSVersion</key>
|
||||||
|
<string>12.0</string>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1
ios/Flutter/Debug.xcconfig
Normal file
@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
1
ios/Flutter/Release.xcconfig
Normal file
@ -0,0 +1 @@
|
|||||||
|
#include "Generated.xcconfig"
|
||||||
616
ios/Runner.xcodeproj/project.pbxproj
Normal file
@ -0,0 +1,616 @@
|
|||||||
|
// !$*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 */; };
|
||||||
|
/* End PBXBuildFile section */
|
||||||
|
|
||||||
|
/* Begin PBXContainerItemProxy section */
|
||||||
|
331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = {
|
||||||
|
isa = PBXContainerItemProxy;
|
||||||
|
containerPortal = 97C146E61CF9000F007C117D /* Project object */;
|
||||||
|
proxyType = 1;
|
||||||
|
remoteGlobalIDString = 97C146ED1CF9000F007C117D;
|
||||||
|
remoteInfo = Runner;
|
||||||
|
};
|
||||||
|
/* End PBXContainerItemProxy section */
|
||||||
|
|
||||||
|
/* Begin PBXCopyFilesBuildPhase section */
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */ = {
|
||||||
|
isa = PBXCopyFilesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
dstPath = "";
|
||||||
|
dstSubfolderSpec = 10;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
name = "Embed Frameworks";
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXCopyFilesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXFileReference section */
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
|
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||||
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */ = {
|
||||||
|
isa = PBXFrameworksBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXFrameworksBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXGroup section */
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
331C807B294A618700263BE5 /* RunnerTests.swift */,
|
||||||
|
);
|
||||||
|
path = RunnerTests;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
|
||||||
|
9740EEB21CF90195004384FC /* Debug.xcconfig */,
|
||||||
|
7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
|
||||||
|
9740EEB31CF90195004384FC /* Generated.xcconfig */,
|
||||||
|
);
|
||||||
|
name = Flutter;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146E51CF9000F007C117D = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
9740EEB11CF90186004384FC /* Flutter */,
|
||||||
|
97C146F01CF9000F007C117D /* Runner */,
|
||||||
|
97C146EF1CF9000F007C117D /* Products */,
|
||||||
|
331C8082294A63A400263BE5 /* RunnerTests */,
|
||||||
|
);
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146EF1CF9000F007C117D /* Products */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146EE1CF9000F007C117D /* Runner.app */,
|
||||||
|
331C8081294A63A400263BE5 /* RunnerTests.xctest */,
|
||||||
|
);
|
||||||
|
name = Products;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146F01CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */,
|
||||||
|
97C146FD1CF9000F007C117D /* Assets.xcassets */,
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */,
|
||||||
|
97C147021CF9000F007C117D /* Info.plist */,
|
||||||
|
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */,
|
||||||
|
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */,
|
||||||
|
74858FAE1ED2DC5600515810 /* AppDelegate.swift */,
|
||||||
|
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */,
|
||||||
|
);
|
||||||
|
path = Runner;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXGroup section */
|
||||||
|
|
||||||
|
/* Begin PBXNativeTarget section */
|
||||||
|
331C8080294A63A400263BE5 /* RunnerTests */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
|
||||||
|
buildPhases = (
|
||||||
|
331C807D294A63A400263BE5 /* Sources */,
|
||||||
|
331C807F294A63A400263BE5 /* Resources */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */,
|
||||||
|
);
|
||||||
|
name = RunnerTests;
|
||||||
|
productName = RunnerTests;
|
||||||
|
productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */;
|
||||||
|
productType = "com.apple.product-type.bundle.unit-test";
|
||||||
|
};
|
||||||
|
97C146ED1CF9000F007C117D /* Runner */ = {
|
||||||
|
isa = PBXNativeTarget;
|
||||||
|
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
|
||||||
|
buildPhases = (
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */,
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */,
|
||||||
|
97C146EB1CF9000F007C117D /* Frameworks */,
|
||||||
|
97C146EC1CF9000F007C117D /* Resources */,
|
||||||
|
9705A1C41CF9048500538489 /* Embed Frameworks */,
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */,
|
||||||
|
);
|
||||||
|
buildRules = (
|
||||||
|
);
|
||||||
|
dependencies = (
|
||||||
|
);
|
||||||
|
name = Runner;
|
||||||
|
productName = Runner;
|
||||||
|
productReference = 97C146EE1CF9000F007C117D /* Runner.app */;
|
||||||
|
productType = "com.apple.product-type.application";
|
||||||
|
};
|
||||||
|
/* End PBXNativeTarget section */
|
||||||
|
|
||||||
|
/* Begin PBXProject section */
|
||||||
|
97C146E61CF9000F007C117D /* Project object */ = {
|
||||||
|
isa = PBXProject;
|
||||||
|
attributes = {
|
||||||
|
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 */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXResourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXShellScriptBuildPhase section */
|
||||||
|
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
"${TARGET_BUILD_DIR}/${INFOPLIST_PATH}",
|
||||||
|
);
|
||||||
|
name = "Thin Binary";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
|
||||||
|
};
|
||||||
|
9740EEB61CF901F6004384FC /* Run Script */ = {
|
||||||
|
isa = PBXShellScriptBuildPhase;
|
||||||
|
alwaysOutOfDate = 1;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
);
|
||||||
|
inputPaths = (
|
||||||
|
);
|
||||||
|
name = "Run Script";
|
||||||
|
outputPaths = (
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
shellPath = /bin/sh;
|
||||||
|
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
|
||||||
|
};
|
||||||
|
/* End PBXShellScriptBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXSourcesBuildPhase section */
|
||||||
|
331C807D294A63A400263BE5 /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
97C146EA1CF9000F007C117D /* Sources */ = {
|
||||||
|
isa = PBXSourcesBuildPhase;
|
||||||
|
buildActionMask = 2147483647;
|
||||||
|
files = (
|
||||||
|
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */,
|
||||||
|
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */,
|
||||||
|
);
|
||||||
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
};
|
||||||
|
/* End PBXSourcesBuildPhase section */
|
||||||
|
|
||||||
|
/* Begin PBXTargetDependency section */
|
||||||
|
331C8086294A63A400263BE5 /* PBXTargetDependency */ = {
|
||||||
|
isa = PBXTargetDependency;
|
||||||
|
target = 97C146ED1CF9000F007C117D /* Runner */;
|
||||||
|
targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */;
|
||||||
|
};
|
||||||
|
/* End PBXTargetDependency section */
|
||||||
|
|
||||||
|
/* Begin PBXVariantGroup section */
|
||||||
|
97C146FA1CF9000F007C117D /* Main.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C146FB1CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = Main.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = {
|
||||||
|
isa = PBXVariantGroup;
|
||||||
|
children = (
|
||||||
|
97C147001CF9000F007C117D /* Base */,
|
||||||
|
);
|
||||||
|
name = LaunchScreen.storyboard;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
/* End PBXVariantGroup section */
|
||||||
|
|
||||||
|
/* Begin XCBuildConfiguration section */
|
||||||
|
249021D3217E4FDB00AE95B9 /* Profile */ = {
|
||||||
|
isa = XCBuildConfiguration;
|
||||||
|
buildSettings = {
|
||||||
|
ALWAYS_SEARCH_USER_PATHS = NO;
|
||||||
|
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.environmentMonitoringApp;
|
||||||
|
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;
|
||||||
|
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.environmentMonitoringApp.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;
|
||||||
|
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.environmentMonitoringApp.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;
|
||||||
|
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.environmentMonitoringApp.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 = AppIcon;
|
||||||
|
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 = AppIcon;
|
||||||
|
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.environmentMonitoringApp;
|
||||||
|
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.environmentMonitoringApp;
|
||||||
|
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 */;
|
||||||
|
}
|
||||||
7
ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "self:">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
101
ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Scheme
|
||||||
|
LastUpgradeVersion = "1510"
|
||||||
|
version = "1.3">
|
||||||
|
<BuildAction
|
||||||
|
parallelizeBuildables = "YES"
|
||||||
|
buildImplicitDependencies = "YES">
|
||||||
|
<BuildActionEntries>
|
||||||
|
<BuildActionEntry
|
||||||
|
buildForTesting = "YES"
|
||||||
|
buildForRunning = "YES"
|
||||||
|
buildForProfiling = "YES"
|
||||||
|
buildForArchiving = "YES"
|
||||||
|
buildForAnalyzing = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildActionEntry>
|
||||||
|
</BuildActionEntries>
|
||||||
|
</BuildAction>
|
||||||
|
<TestAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES">
|
||||||
|
<MacroExpansion>
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</MacroExpansion>
|
||||||
|
<Testables>
|
||||||
|
<TestableReference
|
||||||
|
skipped = "NO"
|
||||||
|
parallelizable = "YES">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "331C8080294A63A400263BE5"
|
||||||
|
BuildableName = "RunnerTests.xctest"
|
||||||
|
BlueprintName = "RunnerTests"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</TestableReference>
|
||||||
|
</Testables>
|
||||||
|
</TestAction>
|
||||||
|
<LaunchAction
|
||||||
|
buildConfiguration = "Debug"
|
||||||
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
|
customLLDBInitFile = "$(SRCROOT)/Flutter/ephemeral/flutter_lldbinit"
|
||||||
|
launchStyle = "0"
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
ignoresPersistentStateOnLaunch = "NO"
|
||||||
|
debugDocumentVersioning = "YES"
|
||||||
|
debugServiceExtension = "internal"
|
||||||
|
enableGPUValidationMode = "1"
|
||||||
|
allowLocationSimulation = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</LaunchAction>
|
||||||
|
<ProfileAction
|
||||||
|
buildConfiguration = "Profile"
|
||||||
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
savedToolIdentifier = ""
|
||||||
|
useCustomWorkingDirectory = "NO"
|
||||||
|
debugDocumentVersioning = "YES">
|
||||||
|
<BuildableProductRunnable
|
||||||
|
runnableDebuggingMode = "0">
|
||||||
|
<BuildableReference
|
||||||
|
BuildableIdentifier = "primary"
|
||||||
|
BlueprintIdentifier = "97C146ED1CF9000F007C117D"
|
||||||
|
BuildableName = "Runner.app"
|
||||||
|
BlueprintName = "Runner"
|
||||||
|
ReferencedContainer = "container:Runner.xcodeproj">
|
||||||
|
</BuildableReference>
|
||||||
|
</BuildableProductRunnable>
|
||||||
|
</ProfileAction>
|
||||||
|
<AnalyzeAction
|
||||||
|
buildConfiguration = "Debug">
|
||||||
|
</AnalyzeAction>
|
||||||
|
<ArchiveAction
|
||||||
|
buildConfiguration = "Release"
|
||||||
|
revealArchiveInOrganizer = "YES">
|
||||||
|
</ArchiveAction>
|
||||||
|
</Scheme>
|
||||||
7
ios/Runner.xcworkspace/contents.xcworkspacedata
generated
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<Workspace
|
||||||
|
version = "1.0">
|
||||||
|
<FileRef
|
||||||
|
location = "group:Runner.xcodeproj">
|
||||||
|
</FileRef>
|
||||||
|
</Workspace>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>IDEDidComputeMac32BitWarning</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>PreviewsEnabled</key>
|
||||||
|
<false/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
13
ios/Runner/AppDelegate.swift
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
@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)
|
||||||
|
}
|
||||||
|
}
|
||||||
122
ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json
Normal file
@ -0,0 +1,122 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-20x20@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-29x29@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-40x40@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "60x60",
|
||||||
|
"idiom" : "iphone",
|
||||||
|
"filename" : "Icon-App-60x60@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "20x20",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-20x20@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "29x29",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-29x29@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "40x40",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-40x40@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "76x76",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-76x76@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "83.5x83.5",
|
||||||
|
"idiom" : "ipad",
|
||||||
|
"filename" : "Icon-App-83.5x83.5@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"size" : "1024x1024",
|
||||||
|
"idiom" : "ios-marketing",
|
||||||
|
"filename" : "Icon-App-1024x1024@1x.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
After Width: | Height: | Size: 616 KiB |
|
After Width: | Height: | Size: 927 B |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 1.6 KiB |
|
After Width: | Height: | Size: 4.8 KiB |
|
After Width: | Height: | Size: 9.3 KiB |
|
After Width: | Height: | Size: 2.7 KiB |
|
After Width: | Height: | Size: 8.1 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 3.8 KiB |
|
After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 4.6 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 34 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
|
After Width: | Height: | Size: 22 KiB |
|
After Width: | Height: | Size: 7.5 KiB |
|
After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 29 KiB |
23
ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage.png",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@2x.png",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"filename" : "LaunchImage@3x.png",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"version" : 1,
|
||||||
|
"author" : "xcode"
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
BIN
ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
vendored
Normal file
|
After Width: | Height: | Size: 68 B |
5
ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md
vendored
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# Launch Screen Assets
|
||||||
|
|
||||||
|
You can customize the launch screen with your own desired assets by replacing the image files in this directory.
|
||||||
|
|
||||||
|
You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
|
||||||
37
ios/Runner/Base.lproj/LaunchScreen.storyboard
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="12121" systemVersion="16G29" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="12089"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--View Controller-->
|
||||||
|
<scene sceneID="EHf-IW-A2E">
|
||||||
|
<objects>
|
||||||
|
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="Ydg-fD-yQy"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="xbc-2k-c8Z"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<subviews>
|
||||||
|
<imageView opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" image="LaunchImage" translatesAutoresizingMaskIntoConstraints="NO" id="YRO-k0-Ey4">
|
||||||
|
</imageView>
|
||||||
|
</subviews>
|
||||||
|
<color key="backgroundColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
|
||||||
|
<constraints>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="1a2-6s-vTC"/>
|
||||||
|
<constraint firstItem="YRO-k0-Ey4" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" id="4X2-HB-R7a"/>
|
||||||
|
</constraints>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
<point key="canvasLocation" x="53" y="375"/>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
<resources>
|
||||||
|
<image name="LaunchImage" width="168" height="185"/>
|
||||||
|
</resources>
|
||||||
|
</document>
|
||||||
26
ios/Runner/Base.lproj/Main.storyboard
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="10117" systemVersion="15F34" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" initialViewController="BYZ-38-t0r">
|
||||||
|
<dependencies>
|
||||||
|
<deployment identifier="iOS"/>
|
||||||
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="10085"/>
|
||||||
|
</dependencies>
|
||||||
|
<scenes>
|
||||||
|
<!--Flutter View Controller-->
|
||||||
|
<scene sceneID="tne-QT-ifu">
|
||||||
|
<objects>
|
||||||
|
<viewController id="BYZ-38-t0r" customClass="FlutterViewController" sceneMemberID="viewController">
|
||||||
|
<layoutGuides>
|
||||||
|
<viewControllerLayoutGuide type="top" id="y3c-jy-aDJ"/>
|
||||||
|
<viewControllerLayoutGuide type="bottom" id="wfy-db-euE"/>
|
||||||
|
</layoutGuides>
|
||||||
|
<view key="view" contentMode="scaleToFill" id="8bC-Xf-vdC">
|
||||||
|
<rect key="frame" x="0.0" y="0.0" width="600" height="600"/>
|
||||||
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
|
<color key="backgroundColor" white="1" alpha="1" colorSpace="custom" customColorSpace="calibratedWhite"/>
|
||||||
|
</view>
|
||||||
|
</viewController>
|
||||||
|
<placeholder placeholderIdentifier="IBFirstResponder" id="dkx-z0-nzr" sceneMemberID="firstResponder"/>
|
||||||
|
</objects>
|
||||||
|
</scene>
|
||||||
|
</scenes>
|
||||||
|
</document>
|
||||||
49
ios/Runner/Info.plist
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<key>CFBundleDevelopmentRegion</key>
|
||||||
|
<string>$(DEVELOPMENT_LANGUAGE)</string>
|
||||||
|
<key>CFBundleDisplayName</key>
|
||||||
|
<string>Environment Monitoring App</string>
|
||||||
|
<key>CFBundleExecutable</key>
|
||||||
|
<string>$(EXECUTABLE_NAME)</string>
|
||||||
|
<key>CFBundleIdentifier</key>
|
||||||
|
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
|
||||||
|
<key>CFBundleInfoDictionaryVersion</key>
|
||||||
|
<string>6.0</string>
|
||||||
|
<key>CFBundleName</key>
|
||||||
|
<string>environment_monitoring_app</string>
|
||||||
|
<key>CFBundlePackageType</key>
|
||||||
|
<string>APPL</string>
|
||||||
|
<key>CFBundleShortVersionString</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NAME)</string>
|
||||||
|
<key>CFBundleSignature</key>
|
||||||
|
<string>????</string>
|
||||||
|
<key>CFBundleVersion</key>
|
||||||
|
<string>$(FLUTTER_BUILD_NUMBER)</string>
|
||||||
|
<key>LSRequiresIPhoneOS</key>
|
||||||
|
<true/>
|
||||||
|
<key>UILaunchStoryboardName</key>
|
||||||
|
<string>LaunchScreen</string>
|
||||||
|
<key>UIMainStoryboardFile</key>
|
||||||
|
<string>Main</string>
|
||||||
|
<key>UISupportedInterfaceOrientations</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>UISupportedInterfaceOrientations~ipad</key>
|
||||||
|
<array>
|
||||||
|
<string>UIInterfaceOrientationPortrait</string>
|
||||||
|
<string>UIInterfaceOrientationPortraitUpsideDown</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeLeft</string>
|
||||||
|
<string>UIInterfaceOrientationLandscapeRight</string>
|
||||||
|
</array>
|
||||||
|
<key>CADisableMinimumFrameDurationOnPhone</key>
|
||||||
|
<true/>
|
||||||
|
<key>UIApplicationSupportsIndirectInputEvents</key>
|
||||||
|
<true/>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
1
ios/Runner/Runner-Bridging-Header.h
Normal file
@ -0,0 +1 @@
|
|||||||
|
#import "GeneratedPluginRegistrant.h"
|
||||||
12
ios/RunnerTests/RunnerTests.swift
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
import Flutter
|
||||||
|
import UIKit
|
||||||
|
import XCTest
|
||||||
|
|
||||||
|
class RunnerTests: XCTestCase {
|
||||||
|
|
||||||
|
func testExample() {
|
||||||
|
// If you add code to the Runner application, consider adding tests here.
|
||||||
|
// See https://developer.apple.com/documentation/xctest for more information about using XCTest.
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
223
lib/auth_provider.dart
Normal file
@ -0,0 +1,223 @@
|
|||||||
|
// lib/auth_provider.dart
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:environment_monitoring_app/services/api_service.dart';
|
||||||
|
|
||||||
|
/// A comprehensive provider to manage user authentication, session state,
|
||||||
|
/// and cached master data for offline use.
|
||||||
|
class AuthProvider with ChangeNotifier {
|
||||||
|
final ApiService _apiService = ApiService();
|
||||||
|
final DatabaseHelper _dbHelper = DatabaseHelper();
|
||||||
|
|
||||||
|
// --- Session & Profile State ---
|
||||||
|
String? _jwtToken;
|
||||||
|
String? _userEmail;
|
||||||
|
Map<String, dynamic>? _profileData;
|
||||||
|
bool get isLoggedIn => _jwtToken != null;
|
||||||
|
String? get userEmail => _userEmail;
|
||||||
|
Map<String, dynamic>? get profileData => _profileData;
|
||||||
|
|
||||||
|
// --- App State ---
|
||||||
|
bool _isLoading = true;
|
||||||
|
bool _isFirstLogin = true;
|
||||||
|
DateTime? _lastSyncTimestamp;
|
||||||
|
bool get isLoading => _isLoading;
|
||||||
|
bool get isFirstLogin => _isFirstLogin;
|
||||||
|
DateTime? get lastSyncTimestamp => _lastSyncTimestamp;
|
||||||
|
|
||||||
|
// --- Cached Master Data ---
|
||||||
|
List<Map<String, dynamic>>? _allUsers;
|
||||||
|
List<Map<String, dynamic>>? _tarballStations;
|
||||||
|
List<Map<String, dynamic>>? _manualStations;
|
||||||
|
List<Map<String, dynamic>>? _tarballClassifications;
|
||||||
|
List<Map<String, dynamic>>? _riverManualStations;
|
||||||
|
List<Map<String, dynamic>>? _riverTriennialStations;
|
||||||
|
List<Map<String, dynamic>>? _departments;
|
||||||
|
List<Map<String, dynamic>>? _companies;
|
||||||
|
List<Map<String, dynamic>>? _positions;
|
||||||
|
|
||||||
|
// --- Getters for UI access ---
|
||||||
|
List<Map<String, dynamic>>? get allUsers => _allUsers;
|
||||||
|
List<Map<String, dynamic>>? get tarballStations => _tarballStations;
|
||||||
|
List<Map<String, dynamic>>? get manualStations => _manualStations;
|
||||||
|
List<Map<String, dynamic>>? get tarballClassifications => _tarballClassifications;
|
||||||
|
List<Map<String, dynamic>>? get riverManualStations => _riverManualStations;
|
||||||
|
List<Map<String, dynamic>>? get riverTriennialStations => _riverTriennialStations;
|
||||||
|
List<Map<String, dynamic>>? get departments => _departments;
|
||||||
|
List<Map<String, dynamic>>? get companies => _companies;
|
||||||
|
List<Map<String, dynamic>>? get positions => _positions;
|
||||||
|
|
||||||
|
// --- SharedPreferences Keys (made public for BaseApiService) ---
|
||||||
|
static const String tokenKey = 'jwt_token';
|
||||||
|
static const String userEmailKey = 'user_email';
|
||||||
|
static const String profileDataKey = 'user_profile_data';
|
||||||
|
static const String lastSyncTimestampKey = 'last_sync_timestamp';
|
||||||
|
static const String isFirstLoginKey = 'is_first_login';
|
||||||
|
|
||||||
|
AuthProvider() {
|
||||||
|
debugPrint('AuthProvider: Initializing...');
|
||||||
|
_loadSessionAndSyncData();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads the user session from storage and then triggers a data sync.
|
||||||
|
Future<void> _loadSessionAndSyncData() async {
|
||||||
|
_isLoading = true;
|
||||||
|
notifyListeners();
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
_jwtToken = prefs.getString(tokenKey);
|
||||||
|
_userEmail = prefs.getString(userEmailKey);
|
||||||
|
_isFirstLogin = prefs.getBool(isFirstLoginKey) ?? true;
|
||||||
|
final lastSyncMillis = prefs.getInt(lastSyncTimestampKey);
|
||||||
|
if (lastSyncMillis != null) {
|
||||||
|
_lastSyncTimestamp = DateTime.fromMillisecondsSinceEpoch(lastSyncMillis);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always load from local DB first for instant startup
|
||||||
|
await _loadDataFromCache();
|
||||||
|
|
||||||
|
if (_jwtToken != null) {
|
||||||
|
debugPrint('AuthProvider: Session loaded. Triggering online sync.');
|
||||||
|
// Don't await here to allow the UI to build instantly with cached data
|
||||||
|
syncAllData();
|
||||||
|
} else {
|
||||||
|
debugPrint('AuthProvider: No active session. App is in offline mode.');
|
||||||
|
}
|
||||||
|
|
||||||
|
_isLoading = false;
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The main function to sync all app data. It checks for an internet connection
|
||||||
|
/// and fetches from the server if available, otherwise it relies on the local cache.
|
||||||
|
Future<void> syncAllData({bool forceRefresh = false}) async {
|
||||||
|
final connectivityResult = await Connectivity().checkConnectivity();
|
||||||
|
if (connectivityResult != ConnectivityResult.none) {
|
||||||
|
debugPrint("AuthProvider: Device is ONLINE. Fetching fresh data from server.");
|
||||||
|
await _fetchDataFromServer();
|
||||||
|
} else {
|
||||||
|
debugPrint("AuthProvider: Device is OFFLINE. Data is already loaded from cache.");
|
||||||
|
}
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A dedicated method to refresh only the profile.
|
||||||
|
Future<void> refreshProfile() async {
|
||||||
|
final result = await _apiService.refreshProfile();
|
||||||
|
if (result['success']) {
|
||||||
|
// Update the profile data in the provider state
|
||||||
|
_profileData = result['data'];
|
||||||
|
// Persist the updated profile data in SharedPreferences
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(profileDataKey, jsonEncode(_profileData));
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetches all master data from the server and caches it locally.
|
||||||
|
Future<void> _fetchDataFromServer() async {
|
||||||
|
final result = await _apiService.syncAllData();
|
||||||
|
if (result['success']) {
|
||||||
|
final data = result['data'];
|
||||||
|
_profileData = data['profile'];
|
||||||
|
_allUsers = data['allUsers'] != null ? List<Map<String, dynamic>>.from(data['allUsers']) : null;
|
||||||
|
_tarballStations = data['tarballStations'] != null ? List<Map<String, dynamic>>.from(data['tarballStations']) : null;
|
||||||
|
_manualStations = data['manualStations'] != null ? List<Map<String, dynamic>>.from(data['manualStations']) : null;
|
||||||
|
_tarballClassifications = data['tarballClassifications'] != null ? List<Map<String, dynamic>>.from(data['tarballClassifications']) : null;
|
||||||
|
_riverManualStations = data['riverManualStations'] != null ? List<Map<String, dynamic>>.from(data['riverManualStations']) : null;
|
||||||
|
_riverTriennialStations = data['riverTriennialStations'] != null ? List<Map<String, dynamic>>.from(data['riverTriennialStations']) : null;
|
||||||
|
_departments = data['departments'] != null ? List<Map<String, dynamic>>.from(data['departments']) : null;
|
||||||
|
_companies = data['companies'] != null ? List<Map<String, dynamic>>.from(data['companies']) : null;
|
||||||
|
_positions = data['positions'] != null ? List<Map<String, dynamic>>.from(data['positions']) : null;
|
||||||
|
await setLastSyncTimestamp(DateTime.now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Loads all master data from the local cache using DatabaseHelper.
|
||||||
|
Future<void> _loadDataFromCache() async {
|
||||||
|
_profileData = await _dbHelper.loadProfile();
|
||||||
|
_allUsers = await _dbHelper.loadUsers();
|
||||||
|
_tarballStations = await _dbHelper.loadTarballStations();
|
||||||
|
_manualStations = await _dbHelper.loadManualStations();
|
||||||
|
_tarballClassifications = await _dbHelper.loadTarballClassifications();
|
||||||
|
_riverManualStations = await _dbHelper.loadRiverManualStations();
|
||||||
|
_riverTriennialStations = await _dbHelper.loadRiverTriennialStations();
|
||||||
|
_departments = await _dbHelper.loadDepartments();
|
||||||
|
_companies = await _dbHelper.loadCompanies();
|
||||||
|
_positions = await _dbHelper.loadPositions();
|
||||||
|
debugPrint("AuthProvider: All master data loaded from local DB cache.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Methods for UI interaction ---
|
||||||
|
|
||||||
|
/// Handles the login process, saving session data and triggering a full data sync.
|
||||||
|
Future<void> login(String token, Map<String, dynamic> profile) async {
|
||||||
|
_jwtToken = token;
|
||||||
|
_userEmail = profile['email'];
|
||||||
|
_profileData = profile;
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(tokenKey, token);
|
||||||
|
await prefs.setString(userEmailKey, _userEmail!);
|
||||||
|
await prefs.setString(profileDataKey, jsonEncode(profile));
|
||||||
|
await _dbHelper.saveProfile(profile);
|
||||||
|
|
||||||
|
debugPrint('AuthProvider: Login successful. Session and profile persisted.');
|
||||||
|
await syncAllData(forceRefresh: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setProfileData(Map<String, dynamic> data) async {
|
||||||
|
_profileData = data;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setString(profileDataKey, jsonEncode(data));
|
||||||
|
await _dbHelper.saveProfile(data); // Also save to local DB
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setLastSyncTimestamp(DateTime timestamp) async {
|
||||||
|
_lastSyncTimestamp = timestamp;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setInt(lastSyncTimestampKey, timestamp.millisecondsSinceEpoch);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> setIsFirstLogin(bool value) async {
|
||||||
|
_isFirstLogin = value;
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.setBool(isFirstLoginKey, value);
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> logout() async {
|
||||||
|
debugPrint('AuthProvider: Initiating logout...');
|
||||||
|
_jwtToken = null;
|
||||||
|
_userEmail = null;
|
||||||
|
_profileData = null;
|
||||||
|
_lastSyncTimestamp = null;
|
||||||
|
_isFirstLogin = true;
|
||||||
|
_allUsers = null;
|
||||||
|
_tarballStations = null;
|
||||||
|
_manualStations = null;
|
||||||
|
_tarballClassifications = null;
|
||||||
|
_riverManualStations = null;
|
||||||
|
_riverTriennialStations = null;
|
||||||
|
_departments = null;
|
||||||
|
_companies = null;
|
||||||
|
_positions = null;
|
||||||
|
|
||||||
|
final prefs = await SharedPreferences.getInstance();
|
||||||
|
await prefs.clear();
|
||||||
|
await prefs.setBool(isFirstLoginKey, true);
|
||||||
|
|
||||||
|
debugPrint('AuthProvider: All session and cached data cleared.');
|
||||||
|
notifyListeners();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> resetPassword(String email) {
|
||||||
|
return _apiService.post('auth/forgot-password', {'email': email});
|
||||||
|
}
|
||||||
|
}
|
||||||
284
lib/bluetooth/bluetooth_manager.dart
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
import 'package:flutter/foundation.dart';
|
||||||
|
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
||||||
|
|
||||||
|
//import 'utils/converter.dart';
|
||||||
|
import '../serial/utils/converter.dart';
|
||||||
|
import 'utils/crc_calculator.dart';
|
||||||
|
import 'utils/parameter_helper.dart';
|
||||||
|
|
||||||
|
enum BluetoothConnectionState { disconnected, connecting, connected }
|
||||||
|
|
||||||
|
class BluetoothManager {
|
||||||
|
Timer? _dataRequestTimer;
|
||||||
|
|
||||||
|
final FlutterBluetoothSerial _bluetooth = FlutterBluetoothSerial.instance;
|
||||||
|
BluetoothConnection? _connection;
|
||||||
|
|
||||||
|
// --- State Notifiers ---
|
||||||
|
final ValueNotifier<BluetoothConnectionState> connectionState =
|
||||||
|
ValueNotifier(BluetoothConnectionState.disconnected);
|
||||||
|
// MODIFIED: Changed to ValueNotifier for consistency and reactivity.
|
||||||
|
final ValueNotifier<String?> connectedDeviceName = ValueNotifier(null);
|
||||||
|
// ADDED: Notifier to hold the parsed Sonde ID.
|
||||||
|
final ValueNotifier<String?> sondeId = ValueNotifier(null);
|
||||||
|
|
||||||
|
final StreamController<Map<String, double>> _dataStreamController =
|
||||||
|
StreamController<Map<String, double>>.broadcast();
|
||||||
|
Stream<Map<String, double>> get dataStream => _dataStreamController.stream;
|
||||||
|
|
||||||
|
// --- Internal State ---
|
||||||
|
String? connectedDeviceAddress;
|
||||||
|
int _runningCounter = 0;
|
||||||
|
int _communicationLevel = 0;
|
||||||
|
String? _parentAddress;
|
||||||
|
final List<String> _parameterList = [];
|
||||||
|
|
||||||
|
Future<List<BluetoothDevice>> getPairedDevices() async {
|
||||||
|
try {
|
||||||
|
return await _bluetooth.getBondedDevices();
|
||||||
|
} catch (e) {
|
||||||
|
print("Error getting paired devices: $e");
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> connect(BluetoothDevice device) async {
|
||||||
|
if (connectionState.value == BluetoothConnectionState.connected) return;
|
||||||
|
try {
|
||||||
|
connectionState.value = BluetoothConnectionState.connecting;
|
||||||
|
_connection = await BluetoothConnection.toAddress(device.address);
|
||||||
|
|
||||||
|
// MODIFIED: Set the .value of the notifier.
|
||||||
|
connectedDeviceName.value = device.name;
|
||||||
|
connectedDeviceAddress = device.address;
|
||||||
|
|
||||||
|
connectionState.value = BluetoothConnectionState.connected;
|
||||||
|
_connection!.input!.listen(_onDataReceived).onDone(disconnect);
|
||||||
|
} catch (e) {
|
||||||
|
print("Error connecting: $e");
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void disconnect() {
|
||||||
|
if (connectionState.value != BluetoothConnectionState.disconnected) {
|
||||||
|
stopAutoReading(); // Ensure timer is stopped on disconnect
|
||||||
|
_connection?.dispose();
|
||||||
|
_connection = null;
|
||||||
|
|
||||||
|
// MODIFIED: Reset the .value of the notifiers.
|
||||||
|
connectedDeviceName.value = null;
|
||||||
|
// ADDED: Reset Sonde ID on disconnect.
|
||||||
|
sondeId.value = null;
|
||||||
|
|
||||||
|
connectedDeviceAddress = null;
|
||||||
|
connectionState.value = BluetoothConnectionState.disconnected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Starts a periodic timer that requests data automatically.
|
||||||
|
void startAutoReading({Duration interval = const Duration(seconds: 5)}) {
|
||||||
|
// Cancel any existing timer to prevent duplicates.
|
||||||
|
stopAutoReading();
|
||||||
|
|
||||||
|
// Request the first reading immediately without waiting for the timer.
|
||||||
|
if (connectionState.value == BluetoothConnectionState.connected) {
|
||||||
|
startLiveReading();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start a new timer that calls startLiveReading periodically.
|
||||||
|
_dataRequestTimer = Timer.periodic(interval, (Timer t) {
|
||||||
|
if (connectionState.value == BluetoothConnectionState.connected) {
|
||||||
|
startLiveReading();
|
||||||
|
} else {
|
||||||
|
// If we get disconnected for any reason, stop the timer.
|
||||||
|
stopAutoReading();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Stops the automatic data refresh timer.
|
||||||
|
void stopAutoReading() {
|
||||||
|
_dataRequestTimer?.cancel();
|
||||||
|
_dataRequestTimer = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
void startLiveReading() {
|
||||||
|
if (connectionState.value != BluetoothConnectionState.connected) return;
|
||||||
|
_communicationLevel = 0;
|
||||||
|
_parameterList.clear();
|
||||||
|
_parentAddress = null;
|
||||||
|
_sendCommand(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onDataReceived(Uint8List data) {
|
||||||
|
if (data.isEmpty) return;
|
||||||
|
String responseHex = Converter.byteArrayToHexString(data).toUpperCase();
|
||||||
|
print("Received (Lvl: $_communicationLevel): $responseHex");
|
||||||
|
switch (_communicationLevel) {
|
||||||
|
case 0:
|
||||||
|
_handleResponseLevel0(responseHex);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
_handleResponseLevel1(responseHex);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
_handleResponseLevel2(responseHex);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleResponseLevel0(String responseHex) {
|
||||||
|
try {
|
||||||
|
if (responseHex.length < 94) {
|
||||||
|
print("Level 0 response is too short.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ADDED: Parse Sonde ID (Serial Number) from the response.
|
||||||
|
// This uses the same logic as your SerialManager.
|
||||||
|
String serialHex = responseHex.substring(68, 86);
|
||||||
|
String serialAscii = Converter.hexToAscii(serialHex);
|
||||||
|
sondeId.value = serialAscii;
|
||||||
|
print("Successfully Parsed Sonde ID: ${sondeId.value}");
|
||||||
|
|
||||||
|
// Your existing logic to parse the Parent Address.
|
||||||
|
final int dataBlockLength =
|
||||||
|
int.parse(responseHex.substring(30, 34), radix: 16);
|
||||||
|
if (dataBlockLength == 38) {
|
||||||
|
const int dataBlockStart = 34;
|
||||||
|
const int parentAddressOffset = 52;
|
||||||
|
final int addressStart = dataBlockStart + parentAddressOffset;
|
||||||
|
|
||||||
|
_parentAddress = responseHex.substring(addressStart, addressStart + 8);
|
||||||
|
|
||||||
|
print("Successfully Parsed Parent Address: $_parentAddress");
|
||||||
|
|
||||||
|
if (_parentAddress != "00000000") {
|
||||||
|
_communicationLevel = 1;
|
||||||
|
|
||||||
|
// Give the device a moment before we send the next command.
|
||||||
|
Future.delayed(const Duration(milliseconds: 500)).then((_) {
|
||||||
|
_sendCommand(1); // Move to next step
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print("Error parsing Level 0 response: $e");
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleResponseLevel1(String responseHex) {
|
||||||
|
try {
|
||||||
|
if (responseHex.length < 38) return;
|
||||||
|
final int dataBlockLength =
|
||||||
|
int.parse(responseHex.substring(30, 34), radix: 16);
|
||||||
|
final String parametersDataBlock =
|
||||||
|
responseHex.substring(34, 34 + (dataBlockLength * 2));
|
||||||
|
_parameterList.clear();
|
||||||
|
for (int i = 0; i <= parametersDataBlock.length - 6; i += 6) {
|
||||||
|
String parameterCode = parametersDataBlock.substring(i + 2, i + 6);
|
||||||
|
_parameterList.add(ParameterHelper.getDescription(parameterCode));
|
||||||
|
}
|
||||||
|
print("Parsed Parameters: $_parameterList");
|
||||||
|
_communicationLevel = 2;
|
||||||
|
|
||||||
|
// **** UPDATED CODE ****
|
||||||
|
// Give the device a moment before requesting the final data.
|
||||||
|
Future.delayed(const Duration(milliseconds: 500)).then((_) {
|
||||||
|
_sendCommand(2); // Move to final step
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print("Error parsing Level 1 response: $e");
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleResponseLevel2(String responseHex) {
|
||||||
|
try {
|
||||||
|
if (responseHex.length < 38) return;
|
||||||
|
final int dataBlockLength =
|
||||||
|
int.parse(responseHex.substring(30, 34), radix: 16);
|
||||||
|
final String valuesDataBlock =
|
||||||
|
responseHex.substring(34, 34 + (dataBlockLength * 2));
|
||||||
|
final List<double> parameterValues = [];
|
||||||
|
for (int i = 0; i <= valuesDataBlock.length - 8; i += 8) {
|
||||||
|
String valueHex = valuesDataBlock.substring(i, i + 8);
|
||||||
|
parameterValues.add(Converter.hexToFloat(valueHex));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_parameterList.length == parameterValues.length) {
|
||||||
|
Map<String, double> finalReadings = {};
|
||||||
|
for (int i = 0; i < _parameterList.length; i++) {
|
||||||
|
finalReadings[_parameterList[i]] = parameterValues[i];
|
||||||
|
}
|
||||||
|
print("Final Parsed Readings: $finalReadings");
|
||||||
|
_dataStreamController.add(finalReadings);
|
||||||
|
}
|
||||||
|
// Reset for the next reading sequence
|
||||||
|
_communicationLevel = 0;
|
||||||
|
} catch (e) {
|
||||||
|
print("Error parsing Level 2 response: $e");
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _sendCommand(int level) {
|
||||||
|
if (_connection == null || !_connection!.isConnected) {
|
||||||
|
print("Cannot send command, not connected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String commandHex;
|
||||||
|
if (level == 0) {
|
||||||
|
commandHex = _getCommand0();
|
||||||
|
} else if (level == 1) {
|
||||||
|
commandHex = _getCommand1();
|
||||||
|
} else {
|
||||||
|
commandHex = _getCommand2();
|
||||||
|
}
|
||||||
|
|
||||||
|
Uint8List commandBytes = Converter.hexStringToByteArray(commandHex);
|
||||||
|
String crcHexString = computeCrc16Ccitt(commandBytes);
|
||||||
|
String finalHexPacket = commandHex + crcHexString;
|
||||||
|
Uint8List packetToSend = Converter.hexStringToByteArray(finalHexPacket);
|
||||||
|
|
||||||
|
try {
|
||||||
|
_connection?.output.add(packetToSend);
|
||||||
|
_connection?.output.allSent.then((_) {
|
||||||
|
print("Sent (Lvl: $level): $finalHexPacket");
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
print("Error sending data: $e");
|
||||||
|
disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getCommand0() {
|
||||||
|
String seqNo = (_runningCounter++ & 255).toRadixString(16).padLeft(2, '0');
|
||||||
|
return '7E02${seqNo}0000000002000000200000010000';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getCommand1() {
|
||||||
|
String seqNo = (_runningCounter++ & 255).toRadixString(16).padLeft(2, '0');
|
||||||
|
return '7E02$seqNo${_parentAddress}02000000200000180000';
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getCommand2() {
|
||||||
|
String seqNo = (_runningCounter++ & 255).toRadixString(16).padLeft(2, '0');
|
||||||
|
return '7E02$seqNo${_parentAddress}02000000200000190000';
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
disconnect();
|
||||||
|
_dataStreamController.close();
|
||||||
|
connectionState.dispose();
|
||||||
|
// ADDED: Dispose of the new notifiers to prevent memory leaks.
|
||||||
|
connectedDeviceName.dispose();
|
||||||
|
sondeId.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
38
lib/bluetooth/utils/converter.dart
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
// ADD THIS FUNCTION
|
||||||
|
/// Converts a string of hexadecimal characters to its ASCII equivalent.
|
||||||
|
String hexToAscii(String hex) {
|
||||||
|
hex = hex.replaceAll(" ", "");
|
||||||
|
List<int> bytes = [];
|
||||||
|
for (int i = 0; i < hex.length; i += 2) {
|
||||||
|
try {
|
||||||
|
bytes.add(int.parse(hex.substring(i, i + 2), radix: 16));
|
||||||
|
} catch (e) {
|
||||||
|
// Ignore errors from non-hex characters if any
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ascii.decode(bytes, allowInvalid: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Uint8List hexStringToByteArray(String hex) {
|
||||||
|
hex = hex.replaceAll(" ", "").toUpperCase();
|
||||||
|
if (hex.length % 2 != 0) hex = '0' + hex;
|
||||||
|
return Uint8List.fromList(
|
||||||
|
List.generate(hex.length ~/ 2, (i) => int.parse(hex.substring(i * 2, i * 2 + 2), radix: 16))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String byteArrayToHexString(Uint8List bytes) {
|
||||||
|
return bytes.map((byte) => byte.toRadixString(16).padLeft(2, '0')).join('');
|
||||||
|
}
|
||||||
|
|
||||||
|
double hexToFloat(String hexString) {
|
||||||
|
if (hexString.length != 8) throw ArgumentError("Hex string must be 8 characters for a 32-bit float.");
|
||||||
|
final int intValue = int.parse(hexString, radix: 16);
|
||||||
|
final ByteData byteData = ByteData(4);
|
||||||
|
byteData.setUint32(0, intValue, Endian.big);
|
||||||
|
return byteData.getFloat32(0, Endian.big);
|
||||||
|
}
|
||||||
19
lib/bluetooth/utils/crc_calculator.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
String computeCrc16Ccitt(Uint8List bytes) {
|
||||||
|
int crc = 0xFFFF;
|
||||||
|
int polynomial = 0x1021;
|
||||||
|
|
||||||
|
for (int b in bytes) {
|
||||||
|
for (int i = 0; i < 8; i++) {
|
||||||
|
bool bit = ((b >> (7 - i) & 1) == 1);
|
||||||
|
bool c15 = ((crc >> 15 & 1) == 1);
|
||||||
|
crc <<= 1;
|
||||||
|
if (c15 ^ bit) {
|
||||||
|
crc ^= polynomial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
crc &= 0xffff;
|
||||||
|
return crc.toRadixString(16).padLeft(4, '0').toUpperCase();
|
||||||
|
}
|
||||||
185
lib/bluetooth/utils/parameter_helper.dart
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
/// A utility class that translates YSI instrument hex codes into human-readable descriptions.
|
||||||
|
/// This replaces the `ParameterHelper.java` class using a more efficient Map data structure.
|
||||||
|
class ParameterHelper {
|
||||||
|
/// A private, static, and constant map for maximum performance.
|
||||||
|
/// The map is created only once when the app starts.
|
||||||
|
static const Map<String, String> _parameterMap = {
|
||||||
|
'000100010001': 'Combo Probe for PH and ORP, original HW',
|
||||||
|
'000100010002': 'PH only, Original HW',
|
||||||
|
'000100010003': 'ammonium',
|
||||||
|
'000100010004': 'Potasium',
|
||||||
|
'000100010005': 'Nitrate',
|
||||||
|
'000100010006': 'Chloride',
|
||||||
|
'000100010007': 'Combo Probe for PH and ORP, hybrid HW for PH drift',
|
||||||
|
'000100010008': 'PH only, hybrid HW for PH drift',
|
||||||
|
'000100010009': 'Combo Probe for PH and ORP, VREF offset HW for PH drift',
|
||||||
|
'00010001000A': 'PH only, VREF offset HW for PH drift',
|
||||||
|
'00010001000B': 'Combo Probe for PH and ORP, Floating Reference is at 1.5V',
|
||||||
|
'00010001000C': 'PH only, Floating Reference is at 1.5V',
|
||||||
|
'000100020001': 'Probe for ODO',
|
||||||
|
'000100030001': 'Combo Probe for Conductivity and Temperature',
|
||||||
|
'000100030002': 'Combo Probe for Wet Cal Conductivity and Temperature',
|
||||||
|
'000100030003': 'Combo Probe for High accuracy Conductivity and Temperature',
|
||||||
|
'000100030004': 'Combo Probe for Conductivity and Temperature, Wiped',
|
||||||
|
'00010003FFFF': 'Mfg tester for Conductivity for CT probe',
|
||||||
|
'000100040001': 'Probe for Turbidity',
|
||||||
|
'000100050001': 'EXO1 - 4 port Sonde',
|
||||||
|
'000100050002': 'EXO2 - 6 port Sonde',
|
||||||
|
'000100050003': 'EXO3 - 8 port Sonde',
|
||||||
|
'000100060001': 'Combo of Chlorophyll and BGA-PC',
|
||||||
|
'000100060002': 'Combo of Chlorophyll and BGA-PE',
|
||||||
|
'000100070001': 'depth probe, Non-Vented, Standard 0-250m',
|
||||||
|
'000100070002': 'depth probe, Non-Vented, Medium 0-100m',
|
||||||
|
'000100070003': 'depth probe, Non-Vented, Shallow 0-10m',
|
||||||
|
'000100070004': 'depth probe, Vented, 0-10m',
|
||||||
|
'000100080001': 'Wiper probe',
|
||||||
|
'000100090001': 'Signal Output Adapter - DCP',
|
||||||
|
'0001000A0001': 'Original Handheld (used primarily during manufacturing Testing)',
|
||||||
|
'0001000A0002': 'New Handheld (ProDSS Style) with GPS',
|
||||||
|
'0001000A0003': 'New Handheld (ProDSS Style) w/out GPS',
|
||||||
|
'0001000B0001': 'fDOM',
|
||||||
|
'0001000C0001': 'metal',
|
||||||
|
'0001000E0001': 'EXO Test Board (reserved for R&D)',
|
||||||
|
'0001000E0002': 'High Accuracy CT Test board',
|
||||||
|
'0001000E0003': 'Phloptode',
|
||||||
|
'0001000F0001': 'Deep UV A Channel T Channel wastewater sensor',
|
||||||
|
'0001000F0002': 'Deep UV NOx Nitrate+Nitrite sensor',
|
||||||
|
'000100100001': 'Signal Output Adapter - DCP Gen 2',
|
||||||
|
'000100100002': 'Signal Output Adapter - DCP Gen 2 - Modbus',
|
||||||
|
'000100110001': 'Used on both EXO and DSS platforms.',
|
||||||
|
'000200010001': 'Variable Flourometer Sensor',
|
||||||
|
'000300010001': 'Combo ODO/Conductivity/Temperature',
|
||||||
|
'0000': 'Common Message',
|
||||||
|
'0001': 'Sonde',
|
||||||
|
'0002': 'PH',
|
||||||
|
'0003': 'PH: PH units',
|
||||||
|
'0004': 'PH: mV',
|
||||||
|
'0005': 'ORP',
|
||||||
|
'0006': 'ORP: mV',
|
||||||
|
'0007': 'ORP: mV UnAdjusted',
|
||||||
|
'0008': 'Optical Dissolved Oxygen',
|
||||||
|
'0009': 'Optical Dissolved Oxygen: Compensated % Saturation',
|
||||||
|
'000A': 'Optical Dissolved Oxygen: Compensated mg/L',
|
||||||
|
'000B': 'Internal uP Temp',
|
||||||
|
'000C': 'Internal uP Temp: Degrees Celcius',
|
||||||
|
'000D': 'External Temp',
|
||||||
|
'000E': 'External Temp: Degrees Celcius',
|
||||||
|
'000F': 'Barometer',
|
||||||
|
'0010': 'Barometer: mmHg',
|
||||||
|
'0011': 'Conductivity',
|
||||||
|
'0012': 'Conductivity: us/cm',
|
||||||
|
'0013': 'Conductivity: Salinity',
|
||||||
|
'0014': 'Turbidity',
|
||||||
|
'0015': 'Turbidity: FNU',
|
||||||
|
'0016': 'Turbidity: TSS',
|
||||||
|
'0017': 'Turbidity: RAW',
|
||||||
|
'0018': 'Conductivity: Specific Conductivity',
|
||||||
|
'0019': 'Chlorophyll',
|
||||||
|
'001A': 'Chlorophyll: ug/L',
|
||||||
|
'001B': 'BGA-PE',
|
||||||
|
'001C': 'BGA-PE: cells/mL - replaced with ug/L (0x003B)',
|
||||||
|
'001D': 'BGA-PC',
|
||||||
|
'001E': 'BGA-PC: cells/mL - replaced with ug/L (0x003A)',
|
||||||
|
'001F': 'Chlorophyll: RFU',
|
||||||
|
'0020': 'BGA-PE: RFU',
|
||||||
|
'0021': 'BGA-PC: RFU',
|
||||||
|
'0022': 'Pressure:',
|
||||||
|
'0023': 'Pressure: psi a',
|
||||||
|
'0024': 'Pressure: depth',
|
||||||
|
'0025': 'Etime',
|
||||||
|
'0026': 'Wiper:',
|
||||||
|
'0027': 'Wiper: Position Volts',
|
||||||
|
'0028': 'Sonde: Date and Time',
|
||||||
|
'0029': 'Sonde: Battery Voltage',
|
||||||
|
'002A': 'Sonde: External Voltage',
|
||||||
|
'002B': 'Sonde: Internal Pressure Voltage',
|
||||||
|
'002C': 'fDOM:',
|
||||||
|
'002D': 'fDOM: RAW',
|
||||||
|
'002E': 'fDOM: RFU',
|
||||||
|
'002F': 'Chlorophyll: RAW',
|
||||||
|
'0030': 'BGA-PE: RAW',
|
||||||
|
'0031': 'BGA-PC: RAW',
|
||||||
|
'0032': 'Wiper: Current(mA)',
|
||||||
|
'0033': 'Wiper: Profile Time(s)',
|
||||||
|
'0034': 'Pressure: RAW',
|
||||||
|
'0035': 'ODO: RAW %Sat',
|
||||||
|
'0036': 'ODO: RAW mg/L',
|
||||||
|
'0037': 'fDOM: QSU',
|
||||||
|
'0038': 'Wiper: Peak Current (mA)',
|
||||||
|
'0039': 'Wiper: Parking Retries',
|
||||||
|
'003A': 'BGA-PC: ug/L',
|
||||||
|
'003B': 'BGA-PE: ug/L',
|
||||||
|
'003C': 'GPS Latitude',
|
||||||
|
'003D': 'GPS Longitude',
|
||||||
|
'003E': 'Conductivity: RAW us/cm',
|
||||||
|
'003F': 'Pressure: psi g',
|
||||||
|
'0040': 'Reference Temp',
|
||||||
|
'0041': 'ODO: % EU',
|
||||||
|
'0042': 'Ammonium',
|
||||||
|
'0043': 'Ammonium (NH4+) mV',
|
||||||
|
'0044': 'Ammonium (NH4+) mg/L',
|
||||||
|
'0048': 'Nitrate',
|
||||||
|
'0049': 'Nitrate (NO-3) mV',
|
||||||
|
'004A': 'Nitrate (NO-3) mg/L',
|
||||||
|
'004B': 'Chloride',
|
||||||
|
'004C': 'Chloride (Cl-) mV',
|
||||||
|
'004D': 'Chloride (Cl-) mg/L',
|
||||||
|
'004E': 'Ammonia (NH3)',
|
||||||
|
'004F': 'ODO: % Local',
|
||||||
|
'0050': 'Conductivity: nLF us/cm',
|
||||||
|
'0051': 'Sonde: Sensor Power mW',
|
||||||
|
'0052': 'Sonde: RTC Battery Voltage',
|
||||||
|
'0053': 'Variable Flourescent (VF) meter',
|
||||||
|
'0054': 'VF: Units TBD',
|
||||||
|
'0055': 'Conductivity:TDS mg/L',
|
||||||
|
'0056': 'Pressure: vert pos',
|
||||||
|
'0057': 'GPS Date',
|
||||||
|
'0058': 'GPS Time',
|
||||||
|
'0059': 'GPS Altitude',
|
||||||
|
'005A': 'GPS Speed Over Ground (SOG)',
|
||||||
|
'005B': 'GPS Course Over Ground (COG)',
|
||||||
|
'005C': 'Barometer Temperature',
|
||||||
|
'005D': 'Conductivity Resistance (ohms)',
|
||||||
|
'005E': 'ODO ppm',
|
||||||
|
'005F': 'Turbidity NTU',
|
||||||
|
'0060': 'Temprorary Calibration Value',
|
||||||
|
'0061': 'Sigma',
|
||||||
|
'0062': 'Sigma-T',
|
||||||
|
'0063': 'Deep UV Wastewater: Channel A',
|
||||||
|
'0064': 'Deep UV Wastewater: Channel A Raw',
|
||||||
|
'0065': 'Deep UV Wastewater: Channel A RFU',
|
||||||
|
'0066': 'Deep UV Wastewater: Channel A Concentration',
|
||||||
|
'0067': 'Deep UV Wastewater: Channel T',
|
||||||
|
'0068': 'Deep UV Wastewater: Channel T Raw',
|
||||||
|
'0069': 'Deep UV Wastewater: Channel T RFU',
|
||||||
|
'006A': 'Deep UV Wastewater: Channel T Concentration',
|
||||||
|
'006B': 'Deep UV Wastewater: Ratio',
|
||||||
|
'006C': 'Deep UV NOx Ch238',
|
||||||
|
'006D': 'Deep UV NOx Ch238 Raw',
|
||||||
|
'006E': 'Deep UV NOx Ch238Ref',
|
||||||
|
'006F': 'Deep UV NOx Ch238Ref Raw',
|
||||||
|
'0070': 'Deep UV NOx Ch238 Absorbance',
|
||||||
|
'0071': 'Deep UV NOx Ch238 Absorbance Turb Corrected',
|
||||||
|
'0072': 'Deep UV NOx Ch275',
|
||||||
|
'0073': 'Deep UV NOx Ch275 Raw',
|
||||||
|
'0074': 'Deep UV NOx Ch275Ref',
|
||||||
|
'0075': 'Deep UV NOx Ch275Ref Raw',
|
||||||
|
'0076': 'Deep UV NOx Ch275 Absorbance',
|
||||||
|
'0077': 'Deep UV NOx Ch275 Absorbance Turb Corrected',
|
||||||
|
'0078': 'Deep UV NOx',
|
||||||
|
'0079': 'Deep UV NOx RAW',
|
||||||
|
'007A': 'Deep UV NOx mg/L',
|
||||||
|
'007B': 'fDOM ppb',
|
||||||
|
'9900': 'SOA/DCP',
|
||||||
|
'FF00': 'Handheld',
|
||||||
|
'FF01': 'Hipster',
|
||||||
|
'FF11': 'Conductivity Manufacturing Tester',
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Takes a hex code string and returns its human-readable description.
|
||||||
|
/// If the code is not found, it returns 'UNKNOWN'.
|
||||||
|
static String getDescription(String hexCode) {
|
||||||
|
// The '??' operator provides a default value if the key is not in the map.
|
||||||
|
return _parameterMap[hexCode.toUpperCase()] ?? 'UNKNOWN';
|
||||||
|
}
|
||||||
|
}
|
||||||
44
lib/bluetooth/widgets/bluetooth_device_list_dialog.dart
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bluetooth_serial/flutter_bluetooth_serial.dart';
|
||||||
|
|
||||||
|
/// A dialog that displays a list of paired Bluetooth devices for the user to select.
|
||||||
|
Future<BluetoothDevice?> showBluetoothDeviceListDialog({
|
||||||
|
required BuildContext context,
|
||||||
|
required List<BluetoothDevice> devices,
|
||||||
|
}) {
|
||||||
|
return showDialog<BluetoothDevice>(
|
||||||
|
context: context,
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text('Select a Paired Device'),
|
||||||
|
content: SizedBox(
|
||||||
|
width: double.maxFinite,
|
||||||
|
child: ListView.builder(
|
||||||
|
itemCount: devices.length,
|
||||||
|
shrinkWrap: true,
|
||||||
|
itemBuilder: (BuildContext context, int index) {
|
||||||
|
final device = devices[index];
|
||||||
|
return ListTile(
|
||||||
|
title: Text(device.name ?? 'Unknown Device'),
|
||||||
|
subtitle: Text(device.address),
|
||||||
|
onTap: () {
|
||||||
|
// When a device is tapped, pop the dialog and return the selected device.
|
||||||
|
Navigator.of(context).pop(device);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
actions: <Widget>[
|
||||||
|
TextButton(
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
onPressed: () {
|
||||||
|
// Pop the dialog and return null if canceled.
|
||||||
|
Navigator.of(context).pop(null);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
301
lib/collapsible_sidebar.dart
Normal file
@ -0,0 +1,301 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
// --- Data Structure for Sidebar Menu Items ---
|
||||||
|
class SidebarItem {
|
||||||
|
final IconData? icon; // Now nullable, as we might use an image
|
||||||
|
final String label;
|
||||||
|
final String? route; // Null if it's a parent category
|
||||||
|
final List<SidebarItem>? children; // Sub-items
|
||||||
|
final bool isParent;
|
||||||
|
final String? imagePath; // New: Optional path to an image asset
|
||||||
|
|
||||||
|
SidebarItem({
|
||||||
|
this.icon,
|
||||||
|
required this.label,
|
||||||
|
this.route,
|
||||||
|
this.children,
|
||||||
|
this.isParent = false,
|
||||||
|
this.imagePath, // Initialize the new property
|
||||||
|
}) : assert(icon != null || imagePath != null, 'Either icon or imagePath must be provided'); // Ensure one is present
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Collapsible Sidebar Widget ---
|
||||||
|
class CollapsibleSidebar extends StatefulWidget {
|
||||||
|
final Function(String route) onNavigate;
|
||||||
|
final bool isCollapsed; // Receive collapse state from HomePage
|
||||||
|
final VoidCallback onToggle; // Receive toggle callback from HomePage (for AppBar button)
|
||||||
|
|
||||||
|
const CollapsibleSidebar({
|
||||||
|
super.key,
|
||||||
|
required this.onNavigate,
|
||||||
|
required this.isCollapsed,
|
||||||
|
required this.onToggle,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CollapsibleSidebar> createState() => _CollapsibleSidebarState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CollapsibleSidebarState extends State<CollapsibleSidebar> {
|
||||||
|
// Define your menu items here based on the routes you provided
|
||||||
|
late final List<SidebarItem> _menuItems;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_menuItems = [
|
||||||
|
SidebarItem(
|
||||||
|
// Reverted to IconData for the top-level 'Air' category
|
||||||
|
icon: Icons.cloud,
|
||||||
|
label: "Air",
|
||||||
|
isParent: true,
|
||||||
|
children: [
|
||||||
|
// Added Manual sub-category for Air
|
||||||
|
SidebarItem(
|
||||||
|
icon: Icons.handshake, // Example icon for Manual
|
||||||
|
label: "Manual",
|
||||||
|
isParent: true,
|
||||||
|
children: [
|
||||||
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/manual/dashboard'),
|
||||||
|
SidebarItem(icon: Icons.edit_note, label: "Manual Sampling", route: '/air/manual/manual-sampling'),
|
||||||
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/manual/report'),
|
||||||
|
SidebarItem(icon: Icons.article, label: "Data Log", route: '/air/manual/data-log'),
|
||||||
|
SidebarItem(icon: Icons.image, label: "Image Request", route: '/air/manual/image-request'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SidebarItem(
|
||||||
|
icon: Icons.trending_up, label: "Continuous", isParent: true, children: [
|
||||||
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/continuous/dashboard'),
|
||||||
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/air/continuous/overview'),
|
||||||
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/air/continuous/entry'),
|
||||||
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/continuous/report'),
|
||||||
|
]),
|
||||||
|
SidebarItem(
|
||||||
|
icon: Icons.search, label: "Investigative", isParent: true, children: [
|
||||||
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/investigative/dashboard'),
|
||||||
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/air/investigative/overview'),
|
||||||
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/air/investigative/entry'),
|
||||||
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/investigative/report'),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SidebarItem(
|
||||||
|
// Reverted to IconData for the top-level 'River' category
|
||||||
|
icon: Icons.water,
|
||||||
|
label: "River",
|
||||||
|
isParent: true,
|
||||||
|
children: [
|
||||||
|
// Added Manual sub-category for River
|
||||||
|
SidebarItem(
|
||||||
|
icon: Icons.handshake, // Example icon for Manual
|
||||||
|
label: "Manual",
|
||||||
|
isParent: true,
|
||||||
|
children: [
|
||||||
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/river/manual/dashboard'),
|
||||||
|
SidebarItem(icon: Icons.pin_drop, label: "In-Situ Sampling", route: '/river/manual/in-situ'),
|
||||||
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/manual/report'),
|
||||||
|
SidebarItem(icon: Icons.date_range, label: "Triennial Sampling", route: '/river/manual/triennial'),
|
||||||
|
SidebarItem(icon: Icons.article, label: "Data Log", route: '/river/manual/data-log'),
|
||||||
|
SidebarItem(icon: Icons.image, label: "Image Request", route: '/river/manual/image-request'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SidebarItem(
|
||||||
|
icon: Icons.trending_up, label: "Continuous", isParent: true, children: [
|
||||||
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/river/continuous/dashboard'),
|
||||||
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/river/continuous/overview'),
|
||||||
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/river/continuous/entry'),
|
||||||
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/continuous/report'),
|
||||||
|
]),
|
||||||
|
SidebarItem(
|
||||||
|
icon: Icons.search, label: "Investigative", isParent: true, children: [
|
||||||
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/river/investigative/dashboard'),
|
||||||
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/river/investigative/overview'),
|
||||||
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/river/investigative/entry'),
|
||||||
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/river/investigative/report'),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SidebarItem(
|
||||||
|
// Reverted to IconData for the top-level 'Marine' category
|
||||||
|
icon: Icons.sailing,
|
||||||
|
label: "Marine",
|
||||||
|
isParent: true,
|
||||||
|
children: [
|
||||||
|
// Added Manual sub-category for Marine
|
||||||
|
SidebarItem(
|
||||||
|
icon: Icons.handshake, // Example icon for Manual
|
||||||
|
label: "Manual",
|
||||||
|
isParent: true,
|
||||||
|
children: [
|
||||||
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/marine/manual/dashboard'),
|
||||||
|
SidebarItem(icon: Icons.description, label: "Info Centre Document", route: '/marine/manual/info'),
|
||||||
|
SidebarItem(icon: Icons.assignment, label: "Pre-Sampling", route: '/marine/manual/pre-sampling'),
|
||||||
|
SidebarItem(icon: Icons.pin_drop, label: "In-Situ Sampling", route: '/marine/manual/in-situ'),
|
||||||
|
SidebarItem(icon: Icons.waves, label: "Tarball Sampling", route: '/marine/manual/tarball'),
|
||||||
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/manual/report'),
|
||||||
|
SidebarItem(icon: Icons.article, label: "Data Log", route: '/marine/manual/data-log'),
|
||||||
|
SidebarItem(icon: Icons.image, label: "Image Request", route: '/marine/manual/image-request'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SidebarItem(
|
||||||
|
icon: Icons.trending_up, label: "Continuous", isParent: true, children: [
|
||||||
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/marine/continuous/dashboard'),
|
||||||
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/marine/continuous/overview'),
|
||||||
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/marine/continuous/entry'),
|
||||||
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/continuous/report'),
|
||||||
|
]),
|
||||||
|
SidebarItem(
|
||||||
|
icon: Icons.search, label: "Investigative", isParent: true, children: [
|
||||||
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/marine/investigative/dashboard'),
|
||||||
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/marine/investigative/overview'),
|
||||||
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/marine/investigative/entry'),
|
||||||
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/marine/investigative/report'),
|
||||||
|
]),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
// Added Settings menu item
|
||||||
|
SidebarItem(icon: Icons.settings, label: "Settings", route: '/settings'),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Container(
|
||||||
|
width: widget.isCollapsed ? 70 : 280, // Increased expanded width for sub-menus
|
||||||
|
color: Theme.of(context).primaryColor, // Use theme primary color for sidebar
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16), // Spacing below the (now removed) toggle button area
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: ListView(
|
||||||
|
padding: EdgeInsets.zero, // Remove default listview padding
|
||||||
|
children: [
|
||||||
|
if (!widget.isCollapsed) ...[
|
||||||
|
// If sidebar is expanded, show full categories with sub-menus
|
||||||
|
for (var item in _menuItems)
|
||||||
|
_buildExpandableNavItem(item),
|
||||||
|
] else ...[
|
||||||
|
// If sidebar is collapsed, only show icons for top-level items
|
||||||
|
for (var item in _menuItems)
|
||||||
|
_buildCollapsedNavItem(item),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(color: Colors.white24, height: 1), // Separator before logout
|
||||||
|
// Logout item, using an icon as it's a standard action
|
||||||
|
_buildNavItem(Icons.logout, "Logout", '/logout', isTopLevel: true),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper to build the leading widget (Icon or Image) for a sidebar item
|
||||||
|
Widget _buildLeadingWidget(SidebarItem item) {
|
||||||
|
// Now only checks for icon, as imagePath is not used for top-level items anymore
|
||||||
|
// but the property still exists on SidebarItem for potential future use or other items.
|
||||||
|
return Icon(item.icon, color: Colors.white);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds an expandable item for parent categories (only when sidebar is expanded)
|
||||||
|
Widget _buildExpandableNavItem(SidebarItem item) {
|
||||||
|
if (item.children == null || item.children!.isEmpty) {
|
||||||
|
// This case handles a top-level item that is NOT a parent,
|
||||||
|
// but if you have such an item, it should probably go through _buildNavItem directly.
|
||||||
|
// For this structure, all top-level items are parents.
|
||||||
|
return _buildNavItem(item.icon, item.label, item.route ?? '', isTopLevel: true, imagePath: item.imagePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Theme(
|
||||||
|
data: Theme.of(context).copyWith(dividerColor: Colors.transparent), // Hide divider in expansion tile
|
||||||
|
child: ExpansionTile(
|
||||||
|
initiallyExpanded: false, // You can set this to true if you want some categories open by default
|
||||||
|
leading: _buildLeadingWidget(item), // Use the helper for leading widget
|
||||||
|
title: Text(item.label, style: const TextStyle(color: Colors.white)),
|
||||||
|
iconColor: Colors.white,
|
||||||
|
collapsedIconColor: Colors.white,
|
||||||
|
childrenPadding: const EdgeInsets.only(left: 20.0), // Indent sub-items
|
||||||
|
children: item.children!.map((childItem) {
|
||||||
|
if (childItem.isParent) {
|
||||||
|
// Nested expansion tiles for sub-categories like "Manual", "Continuous"
|
||||||
|
return _buildExpandableNavItem(childItem);
|
||||||
|
} else {
|
||||||
|
// Leaf item (actual navigation link)
|
||||||
|
return _buildNavItem(childItem.icon, childItem.label, childItem.route ?? '', imagePath: childItem.imagePath);
|
||||||
|
}
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds a regular navigation item (for sub-items when expanded, or top-level when collapsed)
|
||||||
|
Widget _buildNavItem(IconData? icon, String label, String route, {bool isTopLevel = false, String? imagePath}) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (route.isNotEmpty) {
|
||||||
|
widget.onNavigate(route);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
vertical: 12,
|
||||||
|
horizontal: isTopLevel && widget.isCollapsed ? 0 : 8, // Center icon if top-level and collapsed
|
||||||
|
),
|
||||||
|
child: isTopLevel && widget.isCollapsed
|
||||||
|
? Center(
|
||||||
|
child: icon != null
|
||||||
|
? Icon(icon, color: Colors.white)
|
||||||
|
: const SizedBox.shrink(), // Fallback if no icon or imagePath
|
||||||
|
) // Only icon when collapsed
|
||||||
|
: Row(
|
||||||
|
children: [
|
||||||
|
icon != null
|
||||||
|
? Icon(icon, color: Colors.white)
|
||||||
|
: const SizedBox.shrink(), // Fallback if no icon or imagePath
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Expanded( // Use Expanded to prevent text overflow
|
||||||
|
child: Text(label, style: const TextStyle(color: Colors.white)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Builds a collapsed navigation item (only icon/image) for top-level categories
|
||||||
|
Widget _buildCollapsedNavItem(SidebarItem item) {
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (item.route != null && item.route!.isNotEmpty) {
|
||||||
|
widget.onNavigate(item.route!);
|
||||||
|
} else {
|
||||||
|
// If a parent item is tapped when collapsed, expand the sidebar
|
||||||
|
widget.onToggle();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 0), // Adjusted vertical padding
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min, // Use minimum space
|
||||||
|
children: [
|
||||||
|
item.icon != null
|
||||||
|
? Icon(item.icon, color: Colors.white, size: 24) // Icon size
|
||||||
|
: const SizedBox.shrink(), // Fallback if no icon
|
||||||
|
const SizedBox(height: 4), // Small space between icon and text
|
||||||
|
Text(
|
||||||
|
item.label,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 10, // Smaller font size to fit
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis, // Handle long labels
|
||||||
|
maxLines: 1, // Ensure it stays on one line
|
||||||
|
textAlign: TextAlign.center, // Center the text
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
105
lib/home_page.dart
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:environment_monitoring_app/auth_provider.dart';
|
||||||
|
import 'package:environment_monitoring_app/collapsible_sidebar.dart'; // Import your sidebar widget
|
||||||
|
|
||||||
|
class HomePage extends StatefulWidget {
|
||||||
|
const HomePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<HomePage> createState() => _HomePageState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _HomePageState extends State<HomePage> {
|
||||||
|
bool _isSidebarCollapsed = true;
|
||||||
|
String _currentSelectedRoute = '/home';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final auth = Provider.of<AuthProvider>(context);
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
leading: IconButton(
|
||||||
|
icon: Icon(_isSidebarCollapsed ? Icons.menu : Icons.close, color: Colors.white),
|
||||||
|
onPressed: () {
|
||||||
|
setState(() {
|
||||||
|
_isSidebarCollapsed = !_isSidebarCollapsed;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
),
|
||||||
|
title: const Text("Environment Monitoring"),
|
||||||
|
actions: [
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(Icons.person),
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.pushNamed(context, '/profile');
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
body: Row(
|
||||||
|
children: [
|
||||||
|
CollapsibleSidebar(
|
||||||
|
isCollapsed: _isSidebarCollapsed,
|
||||||
|
onToggle: () {
|
||||||
|
setState(() {
|
||||||
|
_isSidebarCollapsed = !_isSidebarCollapsed;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onNavigate: (route) {
|
||||||
|
setState(() {
|
||||||
|
_currentSelectedRoute = route;
|
||||||
|
});
|
||||||
|
Navigator.pushNamed(context, route);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Welcome, ${auth.userEmail ?? 'User'}",
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 32),
|
||||||
|
Text(
|
||||||
|
"Select a Department:",
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 16,
|
||||||
|
children: [
|
||||||
|
// Updated navigation to the new department home pages
|
||||||
|
_buildNavButton(context, "Air", Icons.cloud, '/air/home'),
|
||||||
|
_buildNavButton(context, "River", Icons.water, '/river/home'),
|
||||||
|
_buildNavButton(context, "Marine", Icons.sailing, '/marine/home'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNavButton(BuildContext context, String label, IconData icon, String route) {
|
||||||
|
return ElevatedButton.icon(
|
||||||
|
onPressed: () => Navigator.pushNamed(context, route),
|
||||||
|
icon: Icon(icon, size: 24),
|
||||||
|
label: Text(label),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
textStyle: const TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
269
lib/main.dart
Normal file
@ -0,0 +1,269 @@
|
|||||||
|
// lib/main.dart
|
||||||
|
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:provider/provider.dart';
|
||||||
|
import 'package:connectivity_plus/connectivity_plus.dart';
|
||||||
|
|
||||||
|
// CHANGED: Added imports for MultiProvider and the services to be provided.
|
||||||
|
import 'package:provider/single_child_widget.dart';
|
||||||
|
import 'package:environment_monitoring_app/services/local_storage_service.dart';
|
||||||
|
import 'package:environment_monitoring_app/services/river_in_situ_sampling_service.dart';
|
||||||
|
import 'package:environment_monitoring_app/services/telegram_service.dart';
|
||||||
|
|
||||||
|
import 'package:environment_monitoring_app/theme.dart';
|
||||||
|
import 'package:environment_monitoring_app/auth_provider.dart';
|
||||||
|
|
||||||
|
// Core Screens
|
||||||
|
import 'package:environment_monitoring_app/screens/login.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/register.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/forgot_password.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/logout.dart';
|
||||||
|
import 'package:environment_monitoring_app/home_page.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/profile.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/settings.dart';
|
||||||
|
|
||||||
|
// Department Home Pages
|
||||||
|
import 'package:environment_monitoring_app/screens/air/air_home_page.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/river/river_home_page.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/marine_home_page.dart';
|
||||||
|
|
||||||
|
// Air Screens
|
||||||
|
import 'package:environment_monitoring_app/screens/air/manual/air_manual_dashboard.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/air/manual/manual_sampling.dart' as airManualSampling;
|
||||||
|
import 'package:environment_monitoring_app/screens/air/manual/report.dart' as airManualReport;
|
||||||
|
import 'package:environment_monitoring_app/screens/air/manual/data_status_log.dart' as airManualDataStatusLog;
|
||||||
|
import 'package:environment_monitoring_app/screens/air/manual/image_request.dart' as airManualImageRequest;
|
||||||
|
import 'package:environment_monitoring_app/screens/air/continuous/air_continuous_dashboard.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/air/continuous/overview.dart' as airContinuousOverview;
|
||||||
|
import 'package:environment_monitoring_app/screens/air/continuous/entry.dart' as airContinuousEntry;
|
||||||
|
import 'package:environment_monitoring_app/screens/air/continuous/report.dart' as airContinuousReport;
|
||||||
|
import 'package:environment_monitoring_app/screens/air/investigative/air_investigative_dashboard.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/air/investigative/overview.dart' as airInvestigativeOverview;
|
||||||
|
import 'package:environment_monitoring_app/screens/air/investigative/entry.dart' as airInvestigativeEntry;
|
||||||
|
import 'package:environment_monitoring_app/screens/air/investigative/report.dart' as airInvestigativeReport;
|
||||||
|
|
||||||
|
// River Screens
|
||||||
|
import 'package:environment_monitoring_app/screens/river/manual/river_manual_dashboard.dart';
|
||||||
|
// NOTE: This import points to the main stepper screen for the River In-Situ workflow.
|
||||||
|
import 'package:environment_monitoring_app/screens/river/manual/in_situ_sampling.dart' as riverManualInSituSampling;
|
||||||
|
import 'package:environment_monitoring_app/screens/river/manual/data_status_log.dart' as riverManualDataStatusLog;
|
||||||
|
|
||||||
|
//import 'package:environment_monitoring_app/screens/river/manual/in_situ_sampling.dart' as riverManualInSituSampling;
|
||||||
|
import 'package:environment_monitoring_app/screens/river/manual/report.dart' as riverManualReport;
|
||||||
|
import 'package:environment_monitoring_app/screens/river/manual/triennial_sampling.dart' as riverManualTriennialSampling;
|
||||||
|
//import 'package:environment_monitoring_app/screens/river/manual/data_status_log.dart' as riverManualDataStatusLog;
|
||||||
|
import 'package:environment_monitoring_app/screens/river/manual/image_request.dart' as riverManualImageRequest;
|
||||||
|
import 'package:environment_monitoring_app/screens/river/continuous/river_continuous_dashboard.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/river/continuous/overview.dart' as riverContinuousOverview;
|
||||||
|
import 'package:environment_monitoring_app/screens/river/continuous/entry.dart' as riverContinuousEntry;
|
||||||
|
import 'package:environment_monitoring_app/screens/river/continuous/report.dart' as riverContinuousReport;
|
||||||
|
import 'package:environment_monitoring_app/screens/river/investigative/river_investigative_dashboard.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/river/investigative/overview.dart' as riverInvestigativeOverview;
|
||||||
|
import 'package:environment_monitoring_app/screens/river/investigative/entry.dart' as riverInvestigativeEntry;
|
||||||
|
import 'package:environment_monitoring_app/screens/river/investigative/report.dart' as riverInvestigativeReport;
|
||||||
|
|
||||||
|
// Marine Screens
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/manual/marine_manual_dashboard.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/manual/info_centre_document.dart' as marineManualInfoCentreDocument;
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/manual/pre_sampling.dart' as marineManualPreSampling;
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/manual/in_situ_sampling.dart' as marineManualInSituSampling;
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/manual/report.dart' as marineManualReport;
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/manual/data_status_log.dart' as marineManualDataStatusLog;
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/manual/image_request.dart' as marineManualImageRequest;
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/continuous/marine_continuous_dashboard.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/continuous/overview.dart' as marineContinuousOverview;
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/continuous/entry.dart' as marineContinuousEntry;
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/continuous/report.dart' as marineContinuousReport;
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/investigative/marine_investigative_dashboard.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/investigative/overview.dart' as marineInvestigativeOverview;
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/investigative/entry.dart' as marineInvestigativeEntry;
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/investigative/report.dart' as marineInvestigativeReport;
|
||||||
|
|
||||||
|
import 'package:environment_monitoring_app/models/tarball_data.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/manual/tarball_sampling_step1.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/manual/tarball_sampling_step2.dart';
|
||||||
|
import 'package:environment_monitoring_app/screens/marine/manual/tarball_sampling_step3_summary.dart';
|
||||||
|
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
|
||||||
|
setupServices();
|
||||||
|
|
||||||
|
runApp(
|
||||||
|
// CHANGED: Converted to MultiProvider to support all necessary services.
|
||||||
|
MultiProvider(
|
||||||
|
providers: <SingleChildWidget>[
|
||||||
|
// The original AuthProvider
|
||||||
|
ChangeNotifierProvider(create: (_) => AuthProvider()),
|
||||||
|
// Provider for Local Storage Service
|
||||||
|
Provider(create: (_) => LocalStorageService()),
|
||||||
|
// Provider for the River In-Situ Sampling Service
|
||||||
|
Provider(create: (_) => RiverInSituSamplingService()),
|
||||||
|
// NOTE: You would also add your Marine InSituSamplingService here if needed by Provider.
|
||||||
|
],
|
||||||
|
child: const RootApp(),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void setupServices() {
|
||||||
|
final telegramService = TelegramService();
|
||||||
|
|
||||||
|
Future.delayed(const Duration(seconds: 5), () {
|
||||||
|
debugPrint("[Main] Performing initial alert queue processing on app start.");
|
||||||
|
telegramService.processAlertQueue();
|
||||||
|
});
|
||||||
|
|
||||||
|
Connectivity().onConnectivityChanged.listen((List<ConnectivityResult> results) {
|
||||||
|
if (results.contains(ConnectivityResult.mobile) || results.contains(ConnectivityResult.wifi)) {
|
||||||
|
debugPrint("[Main] Internet connection detected. Triggering alert queue processing.");
|
||||||
|
telegramService.processAlertQueue();
|
||||||
|
} else {
|
||||||
|
debugPrint("[Main] Internet connection lost.");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
class RootApp extends StatelessWidget {
|
||||||
|
const RootApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Consumer<AuthProvider>(
|
||||||
|
builder: (context, auth, child) {
|
||||||
|
Widget homeWidget;
|
||||||
|
if (auth.isLoading) {
|
||||||
|
homeWidget = const SplashScreen();
|
||||||
|
} else if (auth.isLoggedIn) {
|
||||||
|
homeWidget = const HomePage();
|
||||||
|
} else {
|
||||||
|
homeWidget = const LoginScreen();
|
||||||
|
}
|
||||||
|
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'Environment Monitoring App',
|
||||||
|
theme: AppTheme.darkBlueTheme,
|
||||||
|
debugShowCheckedModeBanner: false,
|
||||||
|
home: homeWidget,
|
||||||
|
|
||||||
|
onGenerateRoute: (settings) {
|
||||||
|
if (settings.name == '/marine/manual/tarball/step2') {
|
||||||
|
final args = settings.arguments as TarballSamplingData;
|
||||||
|
return MaterialPageRoute(builder: (context) {
|
||||||
|
return TarballSamplingStep2(data: args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (settings.name == '/marine/manual/tarball/step3') {
|
||||||
|
final args = settings.arguments as TarballSamplingData;
|
||||||
|
return MaterialPageRoute(builder: (context) {
|
||||||
|
return TarballSamplingStep3Summary(data: args);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// NOTE: The River In-Situ form uses an internal stepper,
|
||||||
|
// so it does not require onGenerateRoute logic for its steps.
|
||||||
|
return null;
|
||||||
|
},
|
||||||
|
routes: {
|
||||||
|
// Auth Routes
|
||||||
|
'/register': (context) => const RegisterScreen(),
|
||||||
|
'/forgot-password': (context) => ForgotPasswordScreen(),
|
||||||
|
'/logout': (context) => const LogoutScreen(),
|
||||||
|
'/home': (context) => const HomePage(),
|
||||||
|
'/profile': (context) => const ProfileScreen(),
|
||||||
|
'/settings': (context) => const SettingsScreen(),
|
||||||
|
|
||||||
|
// Department Home Pages
|
||||||
|
'/air/home': (context) => const AirHomePage(),
|
||||||
|
'/river/home': (context) => const RiverHomePage(),
|
||||||
|
'/marine/home': (context) => const MarineHomePage(),
|
||||||
|
|
||||||
|
// Air Manual
|
||||||
|
'/air/manual/dashboard': (context) => AirManualDashboard(),
|
||||||
|
'/air/manual/manual-sampling': (context) => airManualSampling.AirManualSampling(),
|
||||||
|
'/air/manual/report': (context) => airManualReport.AirManualReport(),
|
||||||
|
'/air/manual/data-log': (context) => airManualDataStatusLog.AirManualDataStatusLog(),
|
||||||
|
'/air/manual/image-request': (context) => airManualImageRequest.AirManualImageRequest(),
|
||||||
|
|
||||||
|
// Air Continuous
|
||||||
|
'/air/continuous/dashboard': (context) => AirContinuousDashboard(),
|
||||||
|
'/air/continuous/overview': (context) => airContinuousOverview.OverviewScreen(),
|
||||||
|
'/air/continuous/entry': (context) => airContinuousEntry.EntryScreen(),
|
||||||
|
'/air/continuous/report': (context) => airContinuousReport.ReportScreen(),
|
||||||
|
|
||||||
|
// Air Investigative
|
||||||
|
'/air/investigative/dashboard': (context) => AirInvestigativeDashboard(),
|
||||||
|
'/air/investigative/overview': (context) => airInvestigativeOverview.OverviewScreen(),
|
||||||
|
'/air/investigative/entry': (context) => airInvestigativeEntry.EntryScreen(),
|
||||||
|
'/air/investigative/report': (context) => airInvestigativeReport.ReportScreen(),
|
||||||
|
|
||||||
|
// River Manual
|
||||||
|
'/river/manual/dashboard': (context) => RiverManualDashboard(),
|
||||||
|
'/river/manual/in-situ': (context) => riverManualInSituSampling.RiverInSituSamplingScreen(),
|
||||||
|
|
||||||
|
//'/river/manual/in-situ': (context) => riverManualInSituSampling.RiverInSituSampling(),
|
||||||
|
'/river/manual/report': (context) => riverManualReport.RiverManualReport(),
|
||||||
|
'/river/manual/triennial': (context) => riverManualTriennialSampling.RiverTriennialSampling(),
|
||||||
|
'/river/manual/data-log': (context) => riverManualDataStatusLog.RiverDataStatusLog(),
|
||||||
|
//'/river/manual/data-log': (context) => riverManualDataStatusLog.RiverManualDataStatusLog(),
|
||||||
|
'/river/manual/image-request': (context) => riverManualImageRequest.RiverManualImageRequest(),
|
||||||
|
|
||||||
|
// River Continuous
|
||||||
|
'/river/continuous/dashboard': (context) => RiverContinuousDashboard(),
|
||||||
|
'/river/continuous/overview': (context) => riverContinuousOverview.OverviewScreen(),
|
||||||
|
'/river/continuous/entry': (context) => riverContinuousEntry.EntryScreen(),
|
||||||
|
'/river/continuous/report': (context) => riverContinuousReport.ReportScreen(),
|
||||||
|
|
||||||
|
// River Investigative
|
||||||
|
'/river/investigative/dashboard': (context) => RiverInvestigativeDashboard(),
|
||||||
|
'/river/investigative/overview': (context) => riverInvestigativeOverview.OverviewScreen(),
|
||||||
|
'/river/investigative/entry': (context) => riverInvestigativeEntry.EntryScreen(),
|
||||||
|
'/river/investigative/report': (context) => riverInvestigativeReport.ReportScreen(),
|
||||||
|
|
||||||
|
// Marine Manual
|
||||||
|
'/marine/manual/dashboard': (context) => MarineManualDashboard(),
|
||||||
|
'/marine/manual/info': (context) => marineManualInfoCentreDocument.MarineInfoCentreDocument(),
|
||||||
|
'/marine/manual/pre-sampling': (context) => marineManualPreSampling.MarinePreSampling(),
|
||||||
|
'/marine/manual/in-situ': (context) => marineManualInSituSampling.MarineInSituSampling(),
|
||||||
|
'/marine/manual/tarball': (context) => const TarballSamplingStep1(),
|
||||||
|
'/marine/manual/report': (context) => marineManualReport.MarineManualReport(),
|
||||||
|
'/marine/manual/data-log': (context) => marineManualDataStatusLog.MarineManualDataStatusLog(),
|
||||||
|
'/marine/manual/image-request': (context) => marineManualImageRequest.MarineManualImageRequest(),
|
||||||
|
|
||||||
|
// Marine Continuous
|
||||||
|
'/marine/continuous/dashboard': (context) => MarineContinuousDashboard(),
|
||||||
|
'/marine/continuous/overview': (context) => marineContinuousOverview.OverviewScreen(),
|
||||||
|
'/marine/continuous/entry': (context) => marineContinuousEntry.EntryScreen(),
|
||||||
|
'/marine/continuous/report': (context) => marineContinuousReport.ReportScreen(),
|
||||||
|
|
||||||
|
// Marine Investigative
|
||||||
|
'/marine/investigative/dashboard': (context) => MarineInvestigativeDashboard(),
|
||||||
|
'/marine/investigative/overview': (context) => marineInvestigativeOverview.OverviewScreen(),
|
||||||
|
'/marine/investigative/entry': (context) => marineInvestigativeEntry.EntryScreen(),
|
||||||
|
'/marine/investigative/report': (context) => marineInvestigativeReport.ReportScreen(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class SplashScreen extends StatelessWidget {
|
||||||
|
const SplashScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
CircularProgressIndicator(),
|
||||||
|
SizedBox(height: 20),
|
||||||
|
Text('Loading app data...', style: TextStyle(fontSize: 16)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
144
lib/models/in_situ_sampling_data.dart
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// A data model class to hold all information for the multi-step
|
||||||
|
/// In-Situ Sampling form.
|
||||||
|
class InSituSamplingData {
|
||||||
|
// --- Step 1: Sampling & Station Info ---
|
||||||
|
String? firstSamplerName;
|
||||||
|
int? firstSamplerUserId;
|
||||||
|
Map<String, dynamic>? secondSampler;
|
||||||
|
String? samplingDate;
|
||||||
|
String? samplingTime;
|
||||||
|
String? samplingType;
|
||||||
|
String? sampleIdCode;
|
||||||
|
|
||||||
|
String? selectedStateName;
|
||||||
|
String? selectedCategoryName;
|
||||||
|
Map<String, dynamic>? selectedStation;
|
||||||
|
|
||||||
|
String? stationLatitude;
|
||||||
|
String? stationLongitude;
|
||||||
|
String? currentLatitude;
|
||||||
|
String? currentLongitude;
|
||||||
|
double? distanceDifferenceInKm;
|
||||||
|
String? distanceDifferenceRemarks;
|
||||||
|
|
||||||
|
// --- Step 2: Site Info & Photos ---
|
||||||
|
String? weather;
|
||||||
|
String? tideLevel;
|
||||||
|
String? seaCondition;
|
||||||
|
String? eventRemarks;
|
||||||
|
String? labRemarks;
|
||||||
|
|
||||||
|
File? leftLandViewImage;
|
||||||
|
File? rightLandViewImage;
|
||||||
|
File? waterFillingImage;
|
||||||
|
File? seawaterColorImage;
|
||||||
|
File? phPaperImage;
|
||||||
|
|
||||||
|
File? optionalImage1;
|
||||||
|
String? optionalRemark1;
|
||||||
|
File? optionalImage2;
|
||||||
|
String? optionalRemark2;
|
||||||
|
File? optionalImage3;
|
||||||
|
String? optionalRemark3;
|
||||||
|
File? optionalImage4;
|
||||||
|
String? optionalRemark4;
|
||||||
|
|
||||||
|
// --- Step 3: Data Capture ---
|
||||||
|
String? sondeId;
|
||||||
|
String? dataCaptureDate;
|
||||||
|
String? dataCaptureTime;
|
||||||
|
double? oxygenConcentration;
|
||||||
|
double? oxygenSaturation;
|
||||||
|
double? ph;
|
||||||
|
double? salinity;
|
||||||
|
double? electricalConductivity;
|
||||||
|
double? temperature;
|
||||||
|
double? tds;
|
||||||
|
double? turbidity;
|
||||||
|
double? tss;
|
||||||
|
double? batteryVoltage;
|
||||||
|
|
||||||
|
// --- Post-Submission Status ---
|
||||||
|
String? submissionStatus;
|
||||||
|
String? submissionMessage;
|
||||||
|
String? reportId;
|
||||||
|
|
||||||
|
// REPAIRED: Added a constructor to accept initial values.
|
||||||
|
InSituSamplingData({
|
||||||
|
this.samplingDate,
|
||||||
|
this.samplingTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Converts the data model into a Map<String, String> for the API form data.
|
||||||
|
Map<String, String> toApiFormData() {
|
||||||
|
final Map<String, String> map = {};
|
||||||
|
|
||||||
|
void add(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
map[key] = value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 1 Data
|
||||||
|
add('first_sampler_user_id', firstSamplerUserId);
|
||||||
|
add('man_second_sampler_id', secondSampler?['user_id']);
|
||||||
|
add('man_date', samplingDate);
|
||||||
|
add('man_time', samplingTime);
|
||||||
|
add('man_type', samplingType);
|
||||||
|
add('man_sample_id_code', sampleIdCode);
|
||||||
|
add('station_id', selectedStation?['station_id']);
|
||||||
|
add('man_current_latitude', currentLatitude);
|
||||||
|
add('man_current_longitude', currentLongitude);
|
||||||
|
add('man_distance_difference', distanceDifferenceInKm);
|
||||||
|
add('man_distance_difference_remarks', distanceDifferenceRemarks);
|
||||||
|
|
||||||
|
// Step 2 Data
|
||||||
|
add('man_weather', weather);
|
||||||
|
add('man_tide_level', tideLevel);
|
||||||
|
add('man_sea_condition', seaCondition);
|
||||||
|
add('man_event_remark', eventRemarks);
|
||||||
|
add('man_lab_remark', labRemarks);
|
||||||
|
add('man_optional_photo_01_remarks', optionalRemark1);
|
||||||
|
add('man_optional_photo_02_remarks', optionalRemark2);
|
||||||
|
add('man_optional_photo_03_remarks', optionalRemark3);
|
||||||
|
add('man_optional_photo_04_remarks', optionalRemark4);
|
||||||
|
|
||||||
|
// Step 3 Data
|
||||||
|
add('man_sondeID', sondeId);
|
||||||
|
add('data_capture_date', dataCaptureDate);
|
||||||
|
add('data_capture_time', dataCaptureTime);
|
||||||
|
add('man_oxygen_conc', oxygenConcentration);
|
||||||
|
add('man_oxygen_sat', oxygenSaturation);
|
||||||
|
add('man_ph', ph);
|
||||||
|
add('man_salinity', salinity);
|
||||||
|
add('man_conductivity', electricalConductivity);
|
||||||
|
add('man_temperature', temperature);
|
||||||
|
add('man_tds', tds);
|
||||||
|
add('man_turbidity', turbidity);
|
||||||
|
add('man_tss', tss);
|
||||||
|
add('man_battery_volt', batteryVoltage);
|
||||||
|
|
||||||
|
add('first_sampler_name', firstSamplerName);
|
||||||
|
add('man_station_code', selectedStation?['man_station_code']);
|
||||||
|
add('man_station_name', selectedStation?['man_station_name']);
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the image properties into a Map<String, File?> for the multipart API request.
|
||||||
|
Map<String, File?> toApiImageFiles() {
|
||||||
|
return {
|
||||||
|
'man_left_side_land_view': leftLandViewImage,
|
||||||
|
'man_right_side_land_view': rightLandViewImage,
|
||||||
|
'man_filling_water_into_sample_bottle': waterFillingImage,
|
||||||
|
'man_seawater_in_clear_glass_bottle': seawaterColorImage,
|
||||||
|
'man_examine_preservative_ph_paper': phPaperImage,
|
||||||
|
'man_optional_photo_01': optionalImage1,
|
||||||
|
'man_optional_photo_02': optionalImage2,
|
||||||
|
'man_optional_photo_03': optionalImage3,
|
||||||
|
'man_optional_photo_04': optionalImage4,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
164
lib/models/river_in_situ_sampling_data.dart
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
// lib/models/river_in_situ_sampling_data.dart
|
||||||
|
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// A data model class to hold all information for the multi-step
|
||||||
|
/// River In-Situ Sampling form.
|
||||||
|
class RiverInSituSamplingData {
|
||||||
|
// --- Step 1: Sampling & Station Info ---
|
||||||
|
String? firstSamplerName;
|
||||||
|
int? firstSamplerUserId;
|
||||||
|
Map<String, dynamic>? secondSampler;
|
||||||
|
String? samplingDate;
|
||||||
|
String? samplingTime;
|
||||||
|
String? samplingType;
|
||||||
|
String? sampleIdCode;
|
||||||
|
|
||||||
|
String? selectedStateName;
|
||||||
|
String? selectedCategoryName;
|
||||||
|
Map<String, dynamic>? selectedStation;
|
||||||
|
|
||||||
|
String? stationLatitude;
|
||||||
|
String? stationLongitude;
|
||||||
|
String? currentLatitude;
|
||||||
|
String? currentLongitude;
|
||||||
|
double? distanceDifferenceInKm;
|
||||||
|
String? distanceDifferenceRemarks;
|
||||||
|
|
||||||
|
// --- Step 2: Site Info & Photos ---
|
||||||
|
String? weather;
|
||||||
|
// CHANGED: Renamed for river context
|
||||||
|
String? waterLevel;
|
||||||
|
String? riverCondition;
|
||||||
|
String? eventRemarks;
|
||||||
|
String? labRemarks;
|
||||||
|
|
||||||
|
// CHANGED: Image descriptions adapted for river context
|
||||||
|
File? leftBankViewImage;
|
||||||
|
File? rightBankViewImage;
|
||||||
|
File? waterFillingImage;
|
||||||
|
File? waterColorImage;
|
||||||
|
File? phPaperImage;
|
||||||
|
|
||||||
|
File? optionalImage1;
|
||||||
|
String? optionalRemark1;
|
||||||
|
File? optionalImage2;
|
||||||
|
String? optionalRemark2;
|
||||||
|
File? optionalImage3;
|
||||||
|
String? optionalRemark3;
|
||||||
|
File? optionalImage4;
|
||||||
|
String? optionalRemark4;
|
||||||
|
|
||||||
|
// --- Step 3: Data Capture ---
|
||||||
|
String? sondeId;
|
||||||
|
String? dataCaptureDate;
|
||||||
|
String? dataCaptureTime;
|
||||||
|
double? oxygenConcentration;
|
||||||
|
double? oxygenSaturation;
|
||||||
|
double? ph;
|
||||||
|
double? salinity;
|
||||||
|
double? electricalConductivity;
|
||||||
|
double? temperature;
|
||||||
|
double? tds;
|
||||||
|
double? turbidity;
|
||||||
|
double? tss;
|
||||||
|
double? batteryVoltage;
|
||||||
|
|
||||||
|
// --- START: Add your river-specific parameters here ---
|
||||||
|
// Example:
|
||||||
|
// double? flowRate;
|
||||||
|
// --- END: Add your river-specific parameters here ---
|
||||||
|
|
||||||
|
|
||||||
|
// --- Post-Submission Status ---
|
||||||
|
String? submissionStatus;
|
||||||
|
String? submissionMessage;
|
||||||
|
String? reportId;
|
||||||
|
|
||||||
|
RiverInSituSamplingData({
|
||||||
|
this.samplingDate,
|
||||||
|
this.samplingTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Converts the data model into a Map<String, String> for the API form data.
|
||||||
|
Map<String, String> toApiFormData() {
|
||||||
|
final Map<String, String> map = {};
|
||||||
|
|
||||||
|
void add(String key, dynamic value) {
|
||||||
|
if (value != null) {
|
||||||
|
map[key] = value.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// IMPORTANT: The keys below are prefixed with 'r_man_' for river manual sampling.
|
||||||
|
// Ensure these match your backend API requirements.
|
||||||
|
|
||||||
|
// Step 1 Data
|
||||||
|
add('first_sampler_user_id', firstSamplerUserId);
|
||||||
|
add('r_man_second_sampler_id', secondSampler?['user_id']);
|
||||||
|
add('r_man_date', samplingDate);
|
||||||
|
add('r_man_time', samplingTime);
|
||||||
|
add('r_man_type', samplingType);
|
||||||
|
add('r_man_sample_id_code', sampleIdCode);
|
||||||
|
add('station_id', selectedStation?['station_id']);
|
||||||
|
add('r_man_current_latitude', currentLatitude);
|
||||||
|
add('r_man_current_longitude', currentLongitude);
|
||||||
|
add('r_man_distance_difference', distanceDifferenceInKm);
|
||||||
|
add('r_man_distance_difference_remarks', distanceDifferenceRemarks);
|
||||||
|
|
||||||
|
// Step 2 Data
|
||||||
|
add('r_man_weather', weather);
|
||||||
|
add('r_man_water_level', waterLevel);
|
||||||
|
add('r_man_river_condition', riverCondition);
|
||||||
|
add('r_man_event_remark', eventRemarks);
|
||||||
|
add('r_man_lab_remark', labRemarks);
|
||||||
|
add('r_man_optional_photo_01_remarks', optionalRemark1);
|
||||||
|
add('r_man_optional_photo_02_remarks', optionalRemark2);
|
||||||
|
add('r_man_optional_photo_03_remarks', optionalRemark3);
|
||||||
|
add('r_man_optional_photo_04_remarks', optionalRemark4);
|
||||||
|
|
||||||
|
// Step 3 Data
|
||||||
|
add('r_man_sondeID', sondeId);
|
||||||
|
add('data_capture_date', dataCaptureDate);
|
||||||
|
add('data_capture_time', dataCaptureTime);
|
||||||
|
add('r_man_oxygen_conc', oxygenConcentration);
|
||||||
|
add('r_man_oxygen_sat', oxygenSaturation);
|
||||||
|
add('r_man_ph', ph);
|
||||||
|
add('r_man_salinity', salinity);
|
||||||
|
add('r_man_conductivity', electricalConductivity);
|
||||||
|
add('r_man_temperature', temperature);
|
||||||
|
add('r_man_tds', tds);
|
||||||
|
add('r_man_turbidity', turbidity);
|
||||||
|
add('r_man_tss', tss);
|
||||||
|
add('r_man_battery_volt', batteryVoltage);
|
||||||
|
|
||||||
|
// --- START: Add your new river parameters to the form data map ---
|
||||||
|
// Example:
|
||||||
|
// add('r_man_flow_rate', flowRate);
|
||||||
|
// --- END: Add your new river parameters to the form data map ---
|
||||||
|
|
||||||
|
// Additional data for display or logging, adapted from the original model
|
||||||
|
add('first_sampler_name', firstSamplerName);
|
||||||
|
// Assuming river station keys are prefixed with 'r_man_'
|
||||||
|
add('r_man_station_code', selectedStation?['r_man_station_code']);
|
||||||
|
add('r_man_station_name', selectedStation?['r_man_station_name']);
|
||||||
|
|
||||||
|
return map;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts the image properties into a Map<String, File?> for the multipart API request.
|
||||||
|
Map<String, File?> toApiImageFiles() {
|
||||||
|
// IMPORTANT: Keys adapted for river context.
|
||||||
|
return {
|
||||||
|
'r_man_left_bank_view': leftBankViewImage,
|
||||||
|
'r_man_right_bank_view': rightBankViewImage,
|
||||||
|
'r_man_filling_water_into_sample_bottle': waterFillingImage,
|
||||||
|
'r_man_water_in_clear_glass_bottle': waterColorImage,
|
||||||
|
'r_man_examine_preservative_ph_paper': phPaperImage,
|
||||||
|
'r_man_optional_photo_01': optionalImage1,
|
||||||
|
'r_man_optional_photo_02': optionalImage2,
|
||||||
|
'r_man_optional_photo_03': optionalImage3,
|
||||||
|
'r_man_optional_photo_04': optionalImage4,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
83
lib/models/tarball_data.dart
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
/// This class holds all the data collected across the multi-step tarball sampling form.
|
||||||
|
/// It acts as a temporary data container that is passed between screens.
|
||||||
|
class TarballSamplingData {
|
||||||
|
// --- Step 1 Data: Collected in TarballSamplingStep1 ---
|
||||||
|
String? firstSampler;
|
||||||
|
int? firstSamplerUserId;
|
||||||
|
Map<String, dynamic>? secondSampler; // Holds the full user map for the 2nd sampler
|
||||||
|
String? samplingDate;
|
||||||
|
String? samplingTime;
|
||||||
|
String? selectedStateName;
|
||||||
|
String? selectedCategoryName;
|
||||||
|
Map<String, dynamic>? selectedStation; // Holds the full station map
|
||||||
|
String? stationLatitude;
|
||||||
|
String? stationLongitude;
|
||||||
|
String? currentLatitude;
|
||||||
|
String? currentLongitude;
|
||||||
|
double? distanceDifference;
|
||||||
|
|
||||||
|
// --- Step 2 Data: Collected in TarballSamplingStep2 ---
|
||||||
|
int? classificationId; // CORRECTED: Only the ID is needed.
|
||||||
|
File? leftCoastalViewImage;
|
||||||
|
File? rightCoastalViewImage;
|
||||||
|
File? verticalLinesImage;
|
||||||
|
File? horizontalLineImage;
|
||||||
|
File? optionalImage1;
|
||||||
|
String? optionalRemark1;
|
||||||
|
File? optionalImage2;
|
||||||
|
String? optionalRemark2;
|
||||||
|
File? optionalImage3;
|
||||||
|
String? optionalRemark3;
|
||||||
|
File? optionalImage4;
|
||||||
|
String? optionalRemark4;
|
||||||
|
|
||||||
|
// --- Step 3 Data: For handling the submission response ---
|
||||||
|
String? reportId;
|
||||||
|
String? submissionStatus;
|
||||||
|
String? submissionMessage;
|
||||||
|
|
||||||
|
/// Converts the form's text and selection data into a Map suitable for JSON encoding.
|
||||||
|
/// This map will be sent as the body of the first API request.
|
||||||
|
Map<String, String> toFormData() {
|
||||||
|
final Map<String, String> data = {
|
||||||
|
// Required fields
|
||||||
|
'station_id': selectedStation?['station_id']?.toString() ?? '',
|
||||||
|
'sampling_date': samplingDate ?? '',
|
||||||
|
'sampling_time': samplingTime ?? '',
|
||||||
|
|
||||||
|
// User ID fields
|
||||||
|
'first_sampler_user_id': firstSamplerUserId?.toString() ?? '',
|
||||||
|
'second_sampler_user_id': secondSampler?['user_id']?.toString() ?? '',
|
||||||
|
|
||||||
|
// Foreign Key ID for classification
|
||||||
|
'classification_id': classificationId?.toString() ?? '',
|
||||||
|
|
||||||
|
// Other nullable fields
|
||||||
|
'current_latitude': currentLatitude ?? '',
|
||||||
|
'current_longitude': currentLongitude ?? '',
|
||||||
|
'distance_difference': distanceDifference?.toString() ?? '',
|
||||||
|
'optional_photo_remark_01': optionalRemark1 ?? '',
|
||||||
|
'optional_photo_remark_02': optionalRemark2 ?? '',
|
||||||
|
'optional_photo_remark_03': optionalRemark3 ?? '',
|
||||||
|
'optional_photo_remark_04': optionalRemark4 ?? '',
|
||||||
|
};
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gathers all non-null image files into a Map.
|
||||||
|
/// This map is used to build the multipart request for the second API call (image upload).
|
||||||
|
Map<String, File?> toImageFiles() {
|
||||||
|
return {
|
||||||
|
'left_side_coastal_view': leftCoastalViewImage,
|
||||||
|
'right_side_coastal_view': rightCoastalViewImage,
|
||||||
|
'drawing_vertical_lines': verticalLinesImage,
|
||||||
|
'drawing_horizontal_line': horizontalLineImage,
|
||||||
|
'optional_photo_01': optionalImage1,
|
||||||
|
'optional_photo_02': optionalImage2,
|
||||||
|
'optional_photo_03': optionalImage3,
|
||||||
|
'optional_photo_04': optionalImage4,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
164
lib/screens/air/air_home_page.dart
Normal file
@ -0,0 +1,164 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
// Re-defining SidebarItem here for self-containment,
|
||||||
|
// ideally this would be in a shared utility file if used across many screens.
|
||||||
|
class SidebarItem {
|
||||||
|
final IconData? icon;
|
||||||
|
final String label;
|
||||||
|
final String? route;
|
||||||
|
final List<SidebarItem>? children;
|
||||||
|
final bool isParent;
|
||||||
|
final String? imagePath;
|
||||||
|
|
||||||
|
const SidebarItem({
|
||||||
|
this.icon,
|
||||||
|
required this.label,
|
||||||
|
this.route,
|
||||||
|
this.children,
|
||||||
|
this.isParent = false,
|
||||||
|
this.imagePath,
|
||||||
|
}) : assert(icon != null || imagePath != null, 'Either icon or imagePath must be provided');
|
||||||
|
}
|
||||||
|
|
||||||
|
class AirHomePage extends StatelessWidget {
|
||||||
|
const AirHomePage({super.key});
|
||||||
|
|
||||||
|
// Define Air's sub-menu structure (Manual, Continuous, Investigative)
|
||||||
|
// This mirrors the structure from collapsible_sidebar.dart for consistency.
|
||||||
|
final List<SidebarItem> _airSubMenus = const [
|
||||||
|
SidebarItem(
|
||||||
|
icon: Icons.handshake, // Example icon for Manual
|
||||||
|
label: "Manual",
|
||||||
|
isParent: true,
|
||||||
|
children: [
|
||||||
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/manual/dashboard'),
|
||||||
|
SidebarItem(icon: Icons.edit_note, label: "Manual Sampling", route: '/air/manual/manual-sampling'),
|
||||||
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/manual/report'),
|
||||||
|
SidebarItem(icon: Icons.article, label: "Data Log", route: '/air/manual/data-log'),
|
||||||
|
SidebarItem(icon: Icons.image, label: "Image Request", route: '/air/manual/image-request'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SidebarItem(
|
||||||
|
icon: Icons.trending_up,
|
||||||
|
label: "Continuous",
|
||||||
|
isParent: true,
|
||||||
|
children: [
|
||||||
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/continuous/dashboard'),
|
||||||
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/air/continuous/overview'),
|
||||||
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/air/continuous/entry'),
|
||||||
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/continuous/report'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
SidebarItem(
|
||||||
|
icon: Icons.search,
|
||||||
|
label: "Investigative",
|
||||||
|
isParent: true,
|
||||||
|
children: [
|
||||||
|
SidebarItem(icon: Icons.dashboard, label: "Dashboard", route: '/air/investigative/dashboard'),
|
||||||
|
SidebarItem(icon: Icons.info, label: "Overview", route: '/air/investigative/overview'),
|
||||||
|
SidebarItem(icon: Icons.input, label: "Entry", route: '/air/investigative/entry'),
|
||||||
|
SidebarItem(icon: Icons.receipt_long, label: "Report", route: '/air/investigative/report'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: const Text("Air Department"),
|
||||||
|
),
|
||||||
|
body: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.all(24.0),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"Explore Air Monitoring Sections",
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
Column(
|
||||||
|
children: _airSubMenus.map((category) {
|
||||||
|
return _buildCategorySection(context, category);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method to build each category section (Manual, Continuous, Investigative)
|
||||||
|
Widget _buildCategorySection(BuildContext context, SidebarItem category) {
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Category header (icon + label)
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Icon(category.icon, size: 30, color: Theme.of(context).iconTheme.color),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
Text(
|
||||||
|
category.label,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 24, thickness: 1, color: Colors.white24), // Divider below category title
|
||||||
|
// Grid of sub-menu items
|
||||||
|
GridView.builder(
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
|
||||||
|
crossAxisCount: 3, // 3 columns for sub-menu items
|
||||||
|
crossAxisSpacing: 0.0, // Removed horizontal spacing
|
||||||
|
mainAxisSpacing: 0.0, // Removed vertical spacing
|
||||||
|
childAspectRatio: 2.8, // Adjusted aspect ratio for horizontal icon-label layout with bigger content
|
||||||
|
),
|
||||||
|
itemCount: category.children?.length ?? 0,
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final subItem = category.children![index];
|
||||||
|
return InkWell(
|
||||||
|
onTap: () {
|
||||||
|
if (subItem.route != null) {
|
||||||
|
Navigator.pushNamed(context, subItem.route!);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(0), // Removed border radius for seamless grid
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(8.0), // Padding around the row content
|
||||||
|
child: Row( // Changed from Column to Row
|
||||||
|
mainAxisAlignment: MainAxisAlignment.start, // Align content to start
|
||||||
|
children: [
|
||||||
|
subItem.icon != null
|
||||||
|
? Icon(subItem.icon, color: Colors.white70, size: 24) // Increased icon size from 22 to 24
|
||||||
|
: const SizedBox.shrink(),
|
||||||
|
const SizedBox(width: 8), // Space between icon and text (horizontal)
|
||||||
|
Expanded( // Allow text to take remaining space
|
||||||
|
child: Text(
|
||||||
|
subItem.label,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(color: Colors.white70, fontSize: 11), // Increased text size from 10 to 11
|
||||||
|
textAlign: TextAlign.left, // Align text to left
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
maxLines: 1, // Single line for label
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16), // Reduced gap after each category group
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
lib/screens/air/continuous/air_continuous_dashboard.dart
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AirContinuousDashboard extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text("Air Continuous Monitoring")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Continuous Monitoring", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 16,
|
||||||
|
children: [
|
||||||
|
_buildNavButton(context, "Overview", Icons.info, '/air/continuous/overview'),
|
||||||
|
_buildNavButton(context, "Entry", Icons.edit, '/air/continuous/entry'),
|
||||||
|
_buildNavButton(context, "Report", Icons.insert_chart, '/air/continuous/report'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNavButton(BuildContext context, String label, IconData icon, String route) {
|
||||||
|
return ElevatedButton.icon(
|
||||||
|
onPressed: () => Navigator.pushNamed(context, route),
|
||||||
|
icon: Icon(icon),
|
||||||
|
label: Text(label),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
backgroundColor: Colors.blue[800],
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
62
lib/screens/air/continuous/entry.dart
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class EntryScreen extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<EntryScreen> createState() => _EntryScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EntryScreenState extends State<EntryScreen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
String station = '';
|
||||||
|
String parameter = '';
|
||||||
|
String value = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text("Air Continuous Entry")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
Text("Enter Monitoring Data", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(labelText: "Station"),
|
||||||
|
onChanged: (val) => station = val,
|
||||||
|
validator: (val) => val == null || val.isEmpty ? "Required" : null,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(labelText: "Parameter"),
|
||||||
|
onChanged: (val) => parameter = val,
|
||||||
|
validator: (val) => val == null || val.isEmpty ? "Required" : null,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(labelText: "Value"),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
onChanged: (val) => value = val,
|
||||||
|
validator: (val) => val == null || val.isEmpty ? "Required" : null,
|
||||||
|
),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
// Save logic here
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Data submitted")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text("Submit"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
lib/screens/air/continuous/overview.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class OverviewScreen extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text("Air Continuous Overview")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Text(
|
||||||
|
"This screen provides an overview of continuous air monitoring data, including trends, summaries, and station status.",
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
lib/screens/air/continuous/report.dart
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ReportScreen extends StatelessWidget {
|
||||||
|
final List<Map<String, String>> sampleData = [
|
||||||
|
{"Station": "Site A", "Parameter": "PM2.5", "Value": "35"},
|
||||||
|
{"Station": "Site B", "Parameter": "O3", "Value": "22"},
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text("Air Continuous Report")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Monitoring Report", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
DataTable(
|
||||||
|
columns: [
|
||||||
|
DataColumn(label: Text("Station")),
|
||||||
|
DataColumn(label: Text("Parameter")),
|
||||||
|
DataColumn(label: Text("Value")),
|
||||||
|
],
|
||||||
|
rows: sampleData.map((data) {
|
||||||
|
return DataRow(cells: [
|
||||||
|
DataCell(Text(data["Station"]!)),
|
||||||
|
DataCell(Text(data["Parameter"]!)),
|
||||||
|
DataCell(Text(data["Value"]!)),
|
||||||
|
]);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AirInvestigativeDashboard extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text("Air Investigative Study")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Investigative Study", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 16,
|
||||||
|
children: [
|
||||||
|
_buildNavButton(context, "Overview", Icons.info, '/air/investigative/overview'),
|
||||||
|
_buildNavButton(context, "Entry", Icons.edit, '/air/investigative/entry'),
|
||||||
|
_buildNavButton(context, "Report", Icons.insert_chart, '/air/investigative/report'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNavButton(BuildContext context, String label, IconData icon, String route) {
|
||||||
|
return ElevatedButton.icon(
|
||||||
|
onPressed: () => Navigator.pushNamed(context, route),
|
||||||
|
icon: Icon(icon),
|
||||||
|
label: Text(label),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
backgroundColor: Colors.blue[800],
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
lib/screens/air/investigative/entry.dart
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class EntryScreen extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<EntryScreen> createState() => _EntryScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _EntryScreenState extends State<EntryScreen> {
|
||||||
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
String location = '';
|
||||||
|
String pollutant = '';
|
||||||
|
String concentration = '';
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text("Air Investigative Entry")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Form(
|
||||||
|
key: _formKey,
|
||||||
|
child: ListView(
|
||||||
|
children: [
|
||||||
|
Text("Enter Air Study Data", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(labelText: "Location"),
|
||||||
|
onChanged: (val) => location = val,
|
||||||
|
validator: (val) => val == null || val.isEmpty ? "Required" : null,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(labelText: "Pollutant"),
|
||||||
|
onChanged: (val) => pollutant = val,
|
||||||
|
validator: (val) => val == null || val.isEmpty ? "Required" : null,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
TextFormField(
|
||||||
|
decoration: InputDecoration(labelText: "Concentration (µg/m³)"),
|
||||||
|
keyboardType: TextInputType.number,
|
||||||
|
onChanged: (val) => concentration = val,
|
||||||
|
validator: (val) => val == null || val.isEmpty ? "Required" : null,
|
||||||
|
),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
if (_formKey.currentState!.validate()) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Air data submitted")),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Text("Submit"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
17
lib/screens/air/investigative/overview.dart
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class OverviewScreen extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text("Air Investigative Overview")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Text(
|
||||||
|
"This screen provides an overview of investigative air quality studies. These studies are conducted in response to unusual pollution events, complaints, or targeted assessments. Data may include PM levels, VOCs, and meteorological conditions.",
|
||||||
|
style: TextStyle(fontSize: 16),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
39
lib/screens/air/investigative/report.dart
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class ReportScreen extends StatelessWidget {
|
||||||
|
final List<Map<String, String>> sampleData = [
|
||||||
|
{"Location": "Zone A", "Pollutant": "PM10", "Concentration": "45 µg/m³"},
|
||||||
|
{"Location": "Zone B", "Pollutant": "VOC", "Concentration": "12 µg/m³"},
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text("Air Investigative Report")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Investigative Study Report", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
DataTable(
|
||||||
|
columns: [
|
||||||
|
DataColumn(label: Text("Location")),
|
||||||
|
DataColumn(label: Text("Pollutant")),
|
||||||
|
DataColumn(label: Text("Concentration")),
|
||||||
|
],
|
||||||
|
rows: sampleData.map((data) {
|
||||||
|
return DataRow(cells: [
|
||||||
|
DataCell(Text(data["Location"]!)),
|
||||||
|
DataCell(Text(data["Pollutant"]!)),
|
||||||
|
DataCell(Text(data["Concentration"]!)),
|
||||||
|
]);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
42
lib/screens/air/manual/air_manual_dashboard.dart
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AirManualDashboard extends StatelessWidget {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text("Air Manual Sampling")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text("Manual Sampling", style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
Wrap(
|
||||||
|
spacing: 16,
|
||||||
|
runSpacing: 16,
|
||||||
|
children: [
|
||||||
|
_buildNavButton(context, "Overview", Icons.info, '/air/manual/overview'),
|
||||||
|
_buildNavButton(context, "Entry", Icons.edit, '/air/manual/entry'),
|
||||||
|
_buildNavButton(context, "Report", Icons.insert_chart, '/air/manual/report'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildNavButton(BuildContext context, String label, IconData icon, String route) {
|
||||||
|
return ElevatedButton.icon(
|
||||||
|
onPressed: () => Navigator.pushNamed(context, route),
|
||||||
|
icon: Icon(icon),
|
||||||
|
label: Text(label),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 16),
|
||||||
|
backgroundColor: Colors.blue[800],
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
32
lib/screens/air/manual/data_status_log.dart
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
class AirManualDataStatusLog extends StatelessWidget {
|
||||||
|
final List<Map<String, String>> logEntries = [
|
||||||
|
{"Date": "2025-07-15", "Status": "Submitted", "User": "analyst_air"},
|
||||||
|
{"Date": "2025-07-16", "Status": "Approved", "User": "supervisor_air"},
|
||||||
|
];
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text("Air Manual Data Status Log")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: DataTable(
|
||||||
|
columns: [
|
||||||
|
DataColumn(label: Text("Date")),
|
||||||
|
DataColumn(label: Text("Status")),
|
||||||
|
DataColumn(label: Text("User")),
|
||||||
|
],
|
||||||
|
rows: logEntries.map((entry) {
|
||||||
|
return DataRow(cells: [
|
||||||
|
DataCell(Text(entry["Date"]!)),
|
||||||
|
DataCell(Text(entry["Status"]!)),
|
||||||
|
DataCell(Text(entry["User"]!)),
|
||||||
|
]);
|
||||||
|
}).toList(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
lib/screens/air/manual/image_request.dart
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:image_picker/image_picker.dart';
|
||||||
|
import 'dart:io'; // Add this line at the top of these files
|
||||||
|
|
||||||
|
|
||||||
|
class AirManualImageRequest extends StatefulWidget {
|
||||||
|
@override
|
||||||
|
State<AirManualImageRequest> createState() => _AirManualImageRequestState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _AirManualImageRequestState extends State<AirManualImageRequest> {
|
||||||
|
XFile? _image;
|
||||||
|
final picker = ImagePicker();
|
||||||
|
final _descriptionController = TextEditingController();
|
||||||
|
|
||||||
|
Future<void> _pickImage() async {
|
||||||
|
final pickedFile = await picker.pickImage(source: ImageSource.camera);
|
||||||
|
setState(() => _image = pickedFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: Text("Air Manual Image Request")),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(24),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
ElevatedButton.icon(
|
||||||
|
icon: Icon(Icons.camera_alt),
|
||||||
|
label: Text("Capture Image"),
|
||||||
|
onPressed: _pickImage,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
if (_image != null)
|
||||||
|
Image.file(
|
||||||
|
File(_image!.path),
|
||||||
|
height: 200,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
TextField(
|
||||||
|
controller: _descriptionController,
|
||||||
|
decoration: InputDecoration(labelText: "Description"),
|
||||||
|
maxLines: 3,
|
||||||
|
),
|
||||||
|
SizedBox(height: 24),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: () {
|
||||||
|
// Submit logic here
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text("Image request submitted")),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
child: Text("Submit Request"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||