Compare commits

...

10 Commits

69 changed files with 4333 additions and 2018 deletions

1
.gitignore vendored Normal file
View File

@ -0,0 +1 @@
.idea/

158
README.md
View File

@ -1,93 +1,97 @@
# mobile-smartfarming
# Agrilink Vocpro (Smart Farming Application)
A mobile application for managing and monitoring greenhouse conditions with IoT devices using Flutter.
## Overview
This application enables user to monitor greenhouse conditions and control devices like water valves via an IoT interface. The app retrieves data from various sensors (humidity, temperature, etc.) and allows manual control of devices through MQTT.
## Features
- Monitor sensor data (humidity, temperature, light intensity, etc.)
- Control IoT devices (water valves, lights)
- User authentication and profile management
- MQTT integration for real-time data communication (via Rest API)
## Technology Stack
- Flutter (Mobile Framework)
- MQTT (IoT communication)
- REST API (Backend communication)
- GitLab/GitHub for version control
## Flutter Dependency Stack
This project uses several packages to support various functionalities. Below is a list of the key dependencies:
- **[Dio](https://pub.dev/packages/dio)**: A powerful HTTP client for Dart, used for integrating with the API backend.
- **[shared_preferences](https://pub.dev/packages/shared_preferences)**: A package for storing simple data persistently across sessions (e.g., token management).
- **[intl](https://pub.dev/packages/intl)**: Provides internationalization and localization support, including date formatting and number formatting.
- **[go_router](https://pub.dev/packages/go_router)**: A declarative routing package used for managing navigation between screens.
- **[bootstrap_icons](https://pub.dev/packages/bootstrap_icons)**: A package that provides Bootstrap icons for UI components.
- **[fl_chart](https://pub.dev/packages/fl_chart)**: A powerful chart library for visualizing data in different formats, used for sensor data graphing.
- **[flutter_screenutil](https://pub.dev/packages/flutter_screenutil)**: A package to manage screen size responsiveness, ensuring the app looks good on different device resolutions.
- **[gauge_indicator](https://pub.dev/packages/gauge_indicator)**: A widget for visualizing sensor data (e.g., humidity, temperature) with gauge indicators.
- **[mqtt_client](https://pub.dev/packages/mqtt_client)**: A library used for MQTT integration, enabling real-time communication between the app and IoT devices.
## Getting started
To make it easy for you to get started with GitLab, here's a list of recommended next steps.
Already a pro? Just edit this README.md and make it your own. Want to make it easy? [Use the template at the bottom](#editing-this-readme)!
## Add your files
- [ ] [Create](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#create-a-file) or [upload](https://docs.gitlab.com/ee/user/project/repository/web_editor.html#upload-a-file) files
- [ ] [Add files using the command line](https://docs.gitlab.com/ee/gitlab-basics/add-file.html#add-a-file-using-the-command-line) or push an existing Git repository with the following command:
## Installation Guide
1. Clone the repository:
```bash
git clone https://github.com/username/smart-farming.git
```
cd existing_repo
git remote add origin https://gitlab.com/profile-image/kedaireka/smartfarming/mobile-smartfarming.git
git branch -M main
git push -uf origin main
2. Navigate to the project directory:
```bash
cd smart-farming
```
3. Install dependencies:
```bash
flutter pub get
```
if it does't work try:
```bash
flutter outdated
//follow with
flutter pub upgrade --major-versions
```
or check your latest dart version make sure it compatible with this project
4. Run the project:
```bash
flutter run
```
## Integrate with your tools
### 6. **Project Structure**
Berikan deskripsi singkat tentang struktur folder dan file penting di dalam proyek.
- [ ] [Set up project integrations](https://gitlab.com/profile-image/kedaireka/smartfarming/mobile-smartfarming/-/settings/integrations)
## Collaborate with your team
## Project Structure
- `lib/`: Contains the Flutter app source code.
- `core/`: Contains all constant vaalue and data needed for the project.
- `data/`: Conatains model and data response
- `domain/`: logic and service of the application
- `feature/`: Contains every feature in the app
- `provider/`
- `view`
- `widget`
- `pubspec.yaml`: Project dependencies and configurations.
- [ ] [Invite team members and collaborators](https://docs.gitlab.com/ee/user/project/members/)
- [ ] [Create a new merge request](https://docs.gitlab.com/ee/user/project/merge_requests/creating_merge_requests.html)
- [ ] [Automatically close issues from merge requests](https://docs.gitlab.com/ee/user/project/issues/managing_issues.html#closing-issues-automatically)
- [ ] [Enable merge request approvals](https://docs.gitlab.com/ee/user/project/merge_requests/approvals/)
- [ ] [Set auto-merge](https://docs.gitlab.com/ee/user/project/merge_requests/merge_when_pipeline_succeeds.html)
## Test and Deploy
## Progress Report
Use the built-in continuous integration in GitLab.
| Date | Type | Description |
|------------|-------|-----------------------------------------------------------------------------------------------------|
| 2024-10-09 | feat | Completed the integration of MQTT for sensor data and IoT device control. |
| 2024-10-02 | fix | Implemented auto-reconnect for MQTT on app open/close. |
| 2024-09-30 | feat | Added UI for displaying soil pH level with a linear bar. |
| 2024-09-27 | feat | Finalized group display for 'Doses' in ExpansionTile for medicine dosage recommendations. |
| 2024-09-24 | feat | Completed sorting dialog functionality with single sorter selection capability. |
| 2024-09-20 | feat | Implemented patient data creation form with validation and state management using dialogs. |
- [ ] [Get started with GitLab CI/CD](https://docs.gitlab.com/ee/ci/quick_start/index.html)
- [ ] [Analyze your code for known vulnerabilities with Static Application Security Testing (SAST)](https://docs.gitlab.com/ee/user/application_security/sast/)
- [ ] [Deploy to Kubernetes, Amazon EC2, or Amazon ECS using Auto Deploy](https://docs.gitlab.com/ee/topics/autodevops/requirements.html)
- [ ] [Use pull-based deployments for improved Kubernetes management](https://docs.gitlab.com/ee/user/clusters/agent/)
- [ ] [Set up protected environments](https://docs.gitlab.com/ee/ci/environments/protected_environments.html)
***
# Editing this README
When you're ready to make this README your own, just edit this file and use the handy template below (or feel free to structure it however you want - this is just a starting point!). Thanks to [makeareadme.com](https://www.makeareadme.com/) for this template.
## Suggestions for a good README
Every project is different, so consider which of these sections apply to yours. The sections used in the template are suggestions for most open source projects. Also keep in mind that while a README can be too long and detailed, too long is better than too short. If you think your README is too long, consider utilizing another form of documentation rather than cutting out information.
## Name
Choose a self-explaining name for your project.
## Description
Let people know what your project can do specifically. Provide context and add a link to any reference visitors might be unfamiliar with. A list of Features or a Background subsection can also be added here. If there are alternatives to your project, this is a good place to list differentiating factors.
## Badges
On some READMEs, you may see small images that convey metadata, such as whether or not all the tests are passing for the project. You can use Shields to add some to your README. Many services also have instructions for adding a badge.
## Visuals
Depending on what you are making, it can be a good idea to include screenshots or even a video (you'll frequently see GIFs rather than actual videos). Tools like ttygif can help, but check out Asciinema for a more sophisticated method.
## Installation
Within a particular ecosystem, there may be a common way of installing things, such as using Yarn, NuGet, or Homebrew. However, consider the possibility that whoever is reading your README is a novice and would like more guidance. Listing specific steps helps remove ambiguity and gets people to using your project as quickly as possible. If it only runs in a specific context like a particular programming language version or operating system or has dependencies that have to be installed manually, also add a Requirements subsection.
## Usage
Use examples liberally, and show the expected output if you can. It's helpful to have inline the smallest example of usage that you can demonstrate, while providing links to more sophisticated examples if they are too long to reasonably include in the README.
## Support
Tell people where they can go to for help. It can be any combination of an issue tracker, a chat room, an email address, etc.
## Roadmap
If you have ideas for releases in the future, it is a good idea to list them in the README.
## Contributing
State if you are open to contributions and what your requirements are for accepting them.
1. Fork the repository.
2. Create a new branch (`git checkout -b feature/your-feature`).
3. Commit your changes (`git commit -m 'Add your feature'`).
4. Push to the branch (`git push origin feature/your-feature`).
5. Open a Pull Request.
For people who want to make changes to your project, it's helpful to have some documentation on how to get started. Perhaps there is a script that they should run or some environment variables that they need to set. Make these steps explicit. These instructions could also be useful to your future self.
You can also document commands to lint the code or run tests. These steps help to ensure high code quality and reduce the likelihood that the changes inadvertently break something. Having instructions for running tests is especially helpful if it requires external setup, such as starting a Selenium server for testing in a browser.
## Authors and acknowledgment
Show your appreciation to those who have contributed to the project.
## License
For open source projects, say how it is licensed.
## Project status
If you have run out of energy or time for your project, put a note at the top of the README saying that development has slowed down or stopped completely. Someone may choose to fork your project or volunteer to step in as a maintainer or owner, allowing your project to keep going. You can also make an explicit request for maintainers.
This project is licensed under the MIT License.

View File

@ -1,5 +1,8 @@
plugins {
id "com.android.application"
// START: FlutterFire Configuration
id 'com.google.gms.google-services'
// END: FlutterFire Configuration
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
@ -24,7 +27,7 @@ android {
applicationId = "com.pis.agrilink_vocpro"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
minSdk = 21
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName

View File

@ -0,0 +1,29 @@
{
"project_info": {
"project_number": "445047869982",
"project_id": "agrilink-vocpro-b37f9",
"storage_bucket": "agrilink-vocpro-b37f9.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:445047869982:android:d40dc2fd624f86a315540f",
"android_client_info": {
"package_name": "com.pis.agrilink_vocpro"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyCB8i2dE3Oc0kTNtVPw_qSz-T8gPYNjxFk"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@ -19,6 +19,9 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "7.3.0" apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.15" apply false
// END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "1.7.10" apply false
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

View File

@ -0,0 +1,13 @@
{
"type": "service_account",
"project_id": "agrilink-vocpro-b37f9",
"private_key_id": "e6565344e36c0aaf355068ad83cb904b8c0b7f5f",
"private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQC8o5OUuUconoKe\nASbkganjcX7aByEIai3O5XSC5PxKkEzXGgefaHHtNawJO/nhOmsM1Wqd6sBUy8iT\nbH1LI4hKeIS8k/U0n84VgFUnDdScmsgAbC+06BGEncvqElXI6F9LDqdV21qzu7ZC\n4YpkZuTZsoxImaocUIUE0kfIqq4BZc7kkJqaXqYSOSs2wHLUrQT10jezJmw2EXsx\n+cPD2iwjxasP6IaYCylQr+WU+3e5B2+GKjaXPm7p3pLtSNb+rntvBD212R1VDwuT\nZu+Z2t82Hdsm/r+z1RSKqu1pa2hWYFvSiIl1eV+vqCs4MHdGFinhkNZA9o4NCLBV\nixJxiPDtAgMBAAECggEALu9rR7Ce1Cw2Vx51wRrC/MCPPavS6/9RNP6R8gSMAnhr\nagTDu0W3PPxi+Ow3lPLeUlPigna3kpGerxGqDhFB9reMXUhaFRUi52YmKcCu8yUu\n2K4SY6pvO2Ywp6WWuxWYKjpz57CbnYc5xuPLtYYFcGYUsfar5P4izquMaVEQWO/A\nsABd8gnh0oxwigkk1J9c+qtc6ZKwxgSNOeLld4GFw2z9YBqN70QbEvkrB7JA+QdM\nUFL3edSjEazonGdrySbTh/A7l5E31Oft7qS/VpnFyerhGd3AGFU/FVNpohWiSurD\n9O25xt/YIwpMIOxo0/b4jBv2KMLdOr75MV+2tR8iwQKBgQDj9zSMwO6fm3ZfDrkB\nNw2Qba7Tpn5hW5MshyyPVcpiXMuOyZrjsv19IbVHNZ4Hfl6CRQrGWWDs2rNR6+pW\n+zK82HMzrcwl8A7Dn6GCYQt+EHXiPNXlqJy6v83nFD0TU0BgJn4U21KreLyHxriu\nXnJr062bE9k2MRbp3V/HCiwnrQKBgQDT1krQHdHJyVMNDPbU+EHJVQbuJa4HveE+\n4uDW3srH3dzmlbRgji35EA8WOYJQWc2tiDiwuAFwHB0RoI/EGunCgkVT7qDXcbCm\n3MfHJqrlRGamfAmqT+ZrmDN+izrszVJjaUqiv8/4zbUVfiMF7UZkmeVImIa870t4\nrkdnOdIWQQKBgA30mDb1qKYEsl5nXFQCtsXcsRMr4hi7rmrPa57qtZUH8T4wgVbn\nIjWC1sIhOidjSQ3YhCxYVGKrkMa3FoV0o0qERakoPpMj+wNmxlBg+H8jKtRKvR1u\nEQotq8E1r/d65Tn9oUJNmuX8Yi49sBicl4yaTMptkKKMpsZyUuawbLuRAoGALYOw\nZVDS31krR/WOHyoL8HcdWxOrdaxddgaHKoOJ7DGGIeDudYR26jmCFFNc5Wi2IV/Q\nVl/ipzTTJhNqcEccDJeyz5rI4iFiRCfRoL91Hnd42rJx3S73ogx8m+bMTU68MfHQ\ngHFX/OwtySVl3wMhCcJrGhLzeCqt+4rueoZY1kECgYAuN5gouN8YiqtzvrJ+UtUb\n+BqIu0m+DWMBzrJV4391jpnberxTaMVoQIwCMbHYYJ3u8Ur7smFwyrk5zWvTiszx\nNnPfUD4D6BcNbP8+zwNkzEWnfmtQmh5C5V+DrRjxHSgDu/tirzC7WxPnCJlny3eO\nethubLDwgibzNGemftmqXQ==\n-----END PRIVATE KEY-----\n",
"client_email": "firebase-adminsdk-a4f6g@agrilink-vocpro-b37f9.iam.gserviceaccount.com",
"client_id": "109510416441723731803",
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
"token_uri": "https://oauth2.googleapis.com/token",
"auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
"client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/firebase-adminsdk-a4f6g%40agrilink-vocpro-b37f9.iam.gserviceaccount.com",
"universe_domain": "googleapis.com"
}

View File

@ -0,0 +1,3 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"agrilink-vocpro-b37f9","appId":"1:445047869982:android:d40dc2fd624f86a315540f","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"agrilink-vocpro-b37f9","configurations":{"android":"1:445047869982:android:d40dc2fd624f86a315540f","ios":"1:445047869982:ios:9350ac31c363c40415540f"}}}}}}

View File

@ -2,7 +2,7 @@ class AppConstant {
static const String appName = 'Kebun Pintar';
static const String appVersion = '1.0.0';
static const String baseUrl = 'http://192.168.11.41:3333/api/';
static const String baseUrl = 'https://jx027dj4-3333.asse.devtunnels.ms';
static const String mqttServer = 'armadillo.rmq.cloudamqp.com';
static const String mqttUsername = 'obyskxhx:obyskxhx';
@ -10,4 +10,19 @@ class AppConstant {
static const String soilTempInfo =
'Suhu tanah mengacu pada suhu tanah di permukaan atau pada kedalaman tertentu, yang berperan penting dalam pertumbuhan tanaman dan proses pertanian. Suhu ini memengaruhi perkecambahan benih, aktivitas akar, serta penyerapan air dan nutrisi, yang semuanya esensial bagi perkembangan tanaman. Selain itu, suhu tanah juga memengaruhi aktivitas mikroorganisme yang berkontribusi pada kesuburan tanah. Dalam pertanian pintar, sensor suhu tanah sering digunakan untuk memantau dan mengoptimalkan kondisi tanah, memastikan tanaman tumbuh dalam rentang suhu yang ideal.';
static const String npk1 = 'npk1';
static const String npk2 = 'npk2';
static const String dht = 'dht';
static const String soilTemp = 'soilTemperature';
static const String soilMoisture = 'soilMoisture';
static const String airTemp = 'viciTemperature';
static const String humidity = 'humidity';
static const String lightIntensity = 'lightIntensity';
static const String conductivity = 'conductivity';
static const String ph = 'ph';
static const String nitrogen = 'nitrogen';
static const String phosphorus = 'phosphorus';
static const String potassium = 'potassium';
}

View File

@ -10,6 +10,12 @@ String dateFormater(String date) {
return formatter.format(dateTime);
}
String dateFormatterShort(String date) {
final DateTime dateTime = DateTime.parse(date);
final DateFormat formatter = DateFormat('yyyy-MM-dd');
return formatter.format(dateTime);
}
String getGreeting(String time) {
DateTime parsedTime = DateTime.parse(time);
int hour = parsedTime.hour;

View File

@ -2,7 +2,7 @@ import 'package:agrilink_vocpro/features/auth/view/login_screen.dart';
import 'package:agrilink_vocpro/features/dashboard/view/dashboard_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/conductivity/view/conductivity_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/humidity/view/humidity_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/light/view/light_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/luminosity/view/light_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/nitrogen/view/nitrogen_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/ph/view/ph_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/phosphorus/view/phosphorus_screen.dart';
@ -81,77 +81,98 @@ class AppRoute {
static GoRoute buildPotassiumRoute() {
return GoRoute(
path: 'potassium/:value',
path: 'potassium/:value1/:value2',
builder: (context, state) {
final double value =
double.tryParse(state.pathParameters['value'] ?? '') ?? 0.0;
return PotassiumScreen(potassium: value);
final double value1 =
double.tryParse(state.pathParameters['value1'] ?? '') ?? 0.0;
final double value2 =
double.tryParse(state.pathParameters['value2'] ?? '') ?? 0.0;
return PotassiumScreen(potassiumNpk1: value1, potassiumNpk2: value2);
},
);
}
static GoRoute buildPhosphorusRoute() {
return GoRoute(
path: 'phosphorus/:value',
path: 'phosphorus/:value1/:value2',
builder: (context, state) {
final double value =
double.tryParse(state.pathParameters['value'] ?? '') ?? 0.0;
return PhosphorusScreen(phosphorus: value);
final double value1 =
double.tryParse(state.pathParameters['value1'] ?? '') ?? 0.0;
final double value2 =
double.tryParse(state.pathParameters['value2'] ?? '') ?? 0.0;
return PhosphorusScreen(
phosphorusNpk1: value1,
phosphorusNpk2: value2,
);
},
);
}
static GoRoute buildNitrogenRoute() {
return GoRoute(
path: 'nitrogen/:value',
path: 'nitrogen/:value1/:value2',
builder: (context, state) {
final double value =
double.tryParse(state.pathParameters['value'] ?? '') ?? 0.0;
return NitrogenScreen(nitrogen: value);
final double value1 =
double.tryParse(state.pathParameters['value1'] ?? '') ?? 0.0;
final double value2 =
double.tryParse(state.pathParameters['value2'] ?? '') ?? 0.0;
return NitrogenScreen(nitrogenNpk1: value1, nitrogenNpk2: value2);
},
);
}
static GoRoute buildConductivityRoute() {
return GoRoute(
path: 'conductivity/:value',
path: 'conductivity/:value1/:value2',
builder: (context, state) {
final double value =
double.tryParse(state.pathParameters['value'] ?? '') ?? 0.0;
return ConductivityScreen(conductivity: value);
final double value1 =
double.tryParse(state.pathParameters['value1'] ?? '') ?? 0.0;
final double value2 =
double.tryParse(state.pathParameters['value2'] ?? '') ?? 0.0;
return ConductivityScreen(
conductivityNpk1: value1,
conductivityNpk2: value2,
);
},
);
}
static GoRoute buildSoilMoistureRoute() {
return GoRoute(
path: 'soil_moisture/:value',
path: 'soil_moisture/:value1/:value2',
builder: (context, state) {
final double value =
double.tryParse(state.pathParameters['value'] ?? '') ?? 0.0;
return SoilMoistureScreen(moisture: value);
final double value1 =
double.tryParse(state.pathParameters['value1'] ?? '') ?? 0.0;
final double value2 =
double.tryParse(state.pathParameters['value2'] ?? '') ?? 0.0;
return SoilMoistureScreen(moistureNpk1: value1, moistureNpk2: value2);
},
);
}
static GoRoute buildSoilTempRoute() {
return GoRoute(
path: 'soil_temperature/:value',
path: 'soil_temperature/:value1/:value2',
builder: (context, state) {
final double value =
double.tryParse(state.pathParameters['value'] ?? '') ?? 0.0;
return SoilTemperatureScreen(temperature: value);
final double value1 =
double.tryParse(state.pathParameters['value1'] ?? '') ?? 0.0;
final double value2 =
double.tryParse(state.pathParameters['value2'] ?? '') ?? 0.0;
return SoilTemperatureScreen(
npk1Temperature: value1, npk2Temperature: value2);
},
);
}
static GoRoute buildAcidityRoute() {
return GoRoute(
path: 'ph/:value',
path: 'ph/:value1/:value2',
builder: (context, state) {
final double value =
double.tryParse(state.pathParameters['value'] ?? '') ?? 0.0;
return PhScreen(phValue: value);
final double value1 =
double.tryParse(state.pathParameters['value1'] ?? '') ?? 0.0;
final double value2 =
double.tryParse(state.pathParameters['value2'] ?? '') ?? 0.0;
return PhScreen(phValueNpk1: value1, phValueNpk2: value2);
},
);
}

View File

@ -8,11 +8,13 @@ class AppTextfield extends StatelessWidget {
required this.controller,
this.hintText = 'Enter Here',
this.suffixIcon,
this.obscureText = false,
});
final TextEditingController controller;
final String hintText;
final Widget? suffixIcon;
final bool obscureText;
@override
Widget build(BuildContext context) {
@ -29,7 +31,10 @@ class AppTextfield extends StatelessWidget {
),
hintText: hintText,
hintStyle: AppTheme.hintStyle,
suffixIcon: (suffixIcon != null) ? suffixIcon : null),
suffixIcon: (suffixIcon != null) ? suffixIcon : null,
),
onTapOutside: (event) => FocusScope.of(context).unfocus(),
obscureText: obscureText,
);
}
}

View File

@ -0,0 +1,51 @@
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:flutter/material.dart';
class PasswordTextfield extends StatefulWidget {
const PasswordTextfield({
super.key,
required this.controller,
this.hintText = 'Enter Here',
});
final TextEditingController controller;
final String hintText;
@override
State<PasswordTextfield> createState() => _PasswordTextfieldState();
}
class _PasswordTextfieldState extends State<PasswordTextfield> {
bool obscureText = true;
@override
Widget build(BuildContext context) {
return TextField(
controller: widget.controller,
decoration: InputDecoration(
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: AppColor.textDisable, width: 1),
borderRadius: BorderRadius.circular(8),
),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: AppColor.primary, width: 1),
borderRadius: BorderRadius.circular(8),
),
hintText: widget.hintText,
hintStyle: AppTheme.hintStyle,
suffixIconColor: AppColor.textDisable,
suffixIcon: IconButton(
onPressed: () {
setState(() {
obscureText = !obscureText;
});
},
icon: obscureText
? const Icon(Icons.visibility_off)
: const Icon(Icons.visibility)),
),
obscureText: obscureText,
onTapOutside: (event) => FocusScope.of(context).unfocus(),
);
}
}

View File

@ -0,0 +1,48 @@
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
class DhtGraphicResponse {
DataDht? data;
int? statusCode;
String? message;
DhtGraphicResponse({this.data, this.statusCode, this.message});
DhtGraphicResponse.fromJson(Map<String, dynamic> json) {
data = json['data'] != null ? DataDht.fromJson(json['data']) : null;
statusCode = json['statusCode'];
message = json['message'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (this.data != null) {
data['data'] = this.data!.toJson();
}
data['statusCode'] = statusCode;
data['message'] = message;
return data;
}
}
class DataDht {
List<Dht>? dht;
DataDht({this.dht});
DataDht.fromJson(Map<String, dynamic> json) {
if (json['dht'] != null) {
dht = <Dht>[];
json['dht'].forEach((v) {
dht!.add(Dht.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (dht != null) {
data['dht'] = dht!.map((v) => v.toJson()).toList();
}
return data;
}
}

View File

@ -0,0 +1,114 @@
class JwtTokenResponse {
User? user;
int? iat;
int? exp;
JwtTokenResponse({this.user, this.iat, this.exp});
JwtTokenResponse.fromJson(Map<String, dynamic> json) {
user = json['user'] != null ? User.fromJson(json['user']) : null;
iat = json['iat'];
exp = json['exp'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (user != null) {
data['user'] = user!.toJson();
}
data['iat'] = iat;
data['exp'] = exp;
return data;
}
}
class User {
String? id;
String? uroleId;
String? username;
String? email;
String? googleId;
String? fullname;
Null avatar;
bool? isBan;
String? createdAt;
String? updatedAt;
Null deletedAt;
Role? role;
User(
{this.id,
this.uroleId,
this.username,
this.email,
this.googleId,
this.fullname,
this.avatar,
this.isBan,
this.createdAt,
this.updatedAt,
this.deletedAt,
this.role});
User.fromJson(Map<String, dynamic> json) {
id = json['id'];
uroleId = json['urole_id'];
username = json['username'];
email = json['email'];
googleId = json['google_id'];
fullname = json['fullname'];
avatar = json['avatar'];
isBan = json['is_ban'];
createdAt = json['created_at'];
updatedAt = json['updated_at'];
deletedAt = json['deleted_at'];
role = json['role'] != null ? Role.fromJson(json['role']) : null;
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['urole_id'] = uroleId;
data['username'] = username;
data['email'] = email;
data['google_id'] = googleId;
data['fullname'] = fullname;
data['avatar'] = avatar;
data['is_ban'] = isBan;
data['created_at'] = createdAt;
data['updated_at'] = updatedAt;
data['deleted_at'] = deletedAt;
if (role != null) {
data['role'] = role!.toJson();
}
return data;
}
}
class Role {
String? id;
String? code;
String? name;
String? createdAt;
String? updatedAt;
Role({this.id, this.code, this.name, this.createdAt, this.updatedAt});
Role.fromJson(Map<String, dynamic> json) {
id = json['id'];
code = json['code'];
name = json['name'];
createdAt = json['created_at'];
updatedAt = json['updated_at'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['id'] = id;
data['code'] = code;
data['name'] = name;
data['created_at'] = createdAt;
data['updated_at'] = updatedAt;
return data;
}
}

View File

@ -0,0 +1,140 @@
class LatestDataResponse {
Data? data;
int? statusCode;
String? message;
LatestDataResponse({this.data, this.statusCode, this.message});
LatestDataResponse.fromJson(Map<String, dynamic> json) {
data = json['data'] != null ? Data.fromJson(json['data']) : null;
statusCode = json['statusCode'];
message = json['message'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (this.data != null) {
data['data'] = this.data!.toJson();
}
data['statusCode'] = statusCode;
data['message'] = message;
return data;
}
}
class Data {
List<Dht>? dht;
List<Npk>? npk1;
List<Npk>? npk2;
Data({this.dht, this.npk1, this.npk2});
Data.fromJson(Map<String, dynamic> json) {
if (json['dht'] != null) {
dht = <Dht>[];
json['dht'].forEach((v) {
dht!.add(Dht.fromJson(v));
});
}
if (json['npk1'] != null) {
npk1 = <Npk>[];
json['npk1'].forEach((v) {
npk1!.add(Npk.fromJson(v));
});
}
if (json['npk2'] != null) {
npk2 = <Npk>[];
json['npk2'].forEach((v) {
npk2!.add(Npk.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (dht != null) {
data['dht'] = dht!.map((v) => v.toJson()).toList();
}
if (npk1 != null) {
data['npk1'] = npk1!.map((v) => v.toJson()).toList();
}
if (npk2 != null) {
data['npk2'] = npk2!.map((v) => v.toJson()).toList();
}
return data;
}
}
class Dht {
int? hour;
num? vicitemperature;
num? vicihumidity;
num? viciluminosity;
Dht(
{this.hour,
this.vicitemperature,
this.vicihumidity,
this.viciluminosity});
Dht.fromJson(Map<String, dynamic> json) {
hour = json['hour'];
vicitemperature = json['vicitemperature'];
vicihumidity = json['vicihumidity'];
viciluminosity = json['viciluminosity'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['hour'] = hour;
data['vicitemperature'] = vicitemperature;
data['vicihumidity'] = vicihumidity;
data['viciluminosity'] = viciluminosity;
return data;
}
}
class Npk {
int? hour;
num? soiltemperature;
num? soilhumidity;
num? soilconductivity;
num? soilph;
num? soilnitrogen;
num? soilphosphorus;
num? soilpotassium;
Npk(
{this.hour,
this.soiltemperature,
this.soilhumidity,
this.soilconductivity,
this.soilph,
this.soilnitrogen,
this.soilphosphorus,
this.soilpotassium});
Npk.fromJson(Map<String, dynamic> json) {
hour = json['hour'];
soiltemperature = json['soiltemperature'];
soilhumidity = json['soilhumidity'];
soilconductivity = json['soilconductivity'];
soilph = json['soilph'];
soilnitrogen = json['soilnitrogen'];
soilphosphorus = json['soilphosphorus'];
soilpotassium = json['soilpotassium'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['hour'] = hour;
data['soiltemperature'] = soiltemperature;
data['soilhumidity'] = soilhumidity;
data['soilconductivity'] = soilconductivity;
data['soilph'] = soilph;
data['soilnitrogen'] = soilnitrogen;
data['soilphosphorus'] = soilphosphorus;
data['soilpotassium'] = soilpotassium;
return data;
}
}

View File

@ -0,0 +1,42 @@
class LoginResponse {
Data? data;
int? statusCode;
String? message;
LoginResponse({this.data, this.statusCode, this.message});
LoginResponse.fromJson(Map<String, dynamic> json) {
data = json['data'] != null ? Data.fromJson(json['data']) : null;
statusCode = json['statusCode'];
message = json['message'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (this.data != null) {
data['data'] = this.data!.toJson();
}
data['statusCode'] = statusCode;
data['message'] = message;
return data;
}
}
class Data {
String? token;
String? jwtToken;
Data({this.token, this.jwtToken});
Data.fromJson(Map<String, dynamic> json) {
token = json['token'];
jwtToken = json['jwtToken'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['token'] = token;
data['jwtToken'] = jwtToken;
return data;
}
}

View File

@ -0,0 +1,48 @@
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
class Npk1GraphicResponse {
DataNpk1? data;
int? statusCode;
String? message;
Npk1GraphicResponse({this.data, this.statusCode, this.message});
Npk1GraphicResponse.fromJson(Map<String, dynamic> json) {
data = json['data'] != null ? DataNpk1.fromJson(json['data']) : null;
statusCode = json['statusCode'];
message = json['message'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (this.data != null) {
data['data'] = this.data!.toJson();
}
data['statusCode'] = statusCode;
data['message'] = message;
return data;
}
}
class DataNpk1 {
List<Npk>? npk1;
DataNpk1({this.npk1});
DataNpk1.fromJson(Map<String, dynamic> json) {
if (json['npk1'] != null) {
npk1 = <Npk>[];
json['npk1'].forEach((v) {
npk1!.add(Npk.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (npk1 != null) {
data['npk1'] = npk1!.map((v) => v.toJson()).toList();
}
return data;
}
}

View File

@ -0,0 +1,48 @@
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
class Npk2GraphicResponse {
DataNpk2? data;
int? statusCode;
String? message;
Npk2GraphicResponse({this.data, this.statusCode, this.message});
Npk2GraphicResponse.fromJson(Map<String, dynamic> json) {
data = json['data'] != null ? DataNpk2.fromJson(json['data']) : null;
statusCode = json['statusCode'];
message = json['message'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (this.data != null) {
data['data'] = this.data!.toJson();
}
data['statusCode'] = statusCode;
data['message'] = message;
return data;
}
}
class DataNpk2 {
List<Npk>? npk2;
DataNpk2({this.npk2});
DataNpk2.fromJson(Map<String, dynamic> json) {
if (json['npk2'] != null) {
npk2 = <Npk>[];
json['npk2'].forEach((v) {
npk2!.add(Npk.fromJson(v));
});
}
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
if (npk2 != null) {
data['npk2'] = npk2!.map((v) => v.toJson()).toList();
}
return data;
}
}

View File

@ -0,0 +1,18 @@
class SwitchRelayResponse {
bool? success;
String? message;
SwitchRelayResponse({this.success, this.message});
SwitchRelayResponse.fromJson(Map<String, dynamic> json) {
success = json['success'];
message = json['message'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = <String, dynamic>{};
data['success'] = success;
data['message'] = message;
return data;
}
}

View File

@ -1,19 +1,118 @@
import 'dart:convert';
import 'package:agrilink_vocpro/core/constant/app_constant.dart';
import 'package:agrilink_vocpro/core/extension/extention.dart';
import 'package:agrilink_vocpro/data/model/dht_graphic_response.dart';
import 'package:agrilink_vocpro/data/model/jwt_token_response.dart';
import 'package:agrilink_vocpro/data/model/login_response.dart';
import 'package:agrilink_vocpro/data/model/npk1_graphic_response.dart';
import 'package:agrilink_vocpro/data/model/npk2_graphic_response.dart';
import 'package:agrilink_vocpro/data/model/relay_response.dart';
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
import 'package:agrilink_vocpro/data/model/switch_relay_response.dart';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:shared_preferences/shared_preferences.dart';
class AppService {
final Dio _dioWithoutInterceptor = Dio(
BaseOptions(
baseUrl: AppConstant.baseUrl,
connectTimeout: Duration(seconds: 5),
receiveTimeout: Duration(seconds: 5),
),
);
Future<LoginResponse> login({
required String username,
required String password,
String rememberMe = 'false',
}) async {
final SharedPreferences pref = await SharedPreferences.getInstance();
final String basicAuth =
'Basic ${base64Encode(utf8.encode('$username:$password'))}';
FormData formData = FormData.fromMap({
'remember_me': rememberMe,
});
try {
final response = await _dioWithoutInterceptor.post(
'/auth/login',
data: formData,
options: Options(
headers: {
'Authorization': basicAuth,
},
),
);
if (response.statusCode == 200) {
final data = LoginResponse.fromJson(response.data);
final decodedToken =
JwtTokenResponse.fromJson(JwtDecoder.decode(data.data!.jwtToken!));
pref.setString('token', data.data!.token!);
pref.setString('jwtToken', data.data!.jwtToken!);
pref.setString('username', decodedToken.user?.username ?? 'unknown');
pref.setString('email', decodedToken.user?.email ?? 'unknown');
pref.setString('fullName', decodedToken.user?.fullname ?? 'unknown');
pref.setBool('isLoggedIn', true);
return data;
} else {
throw Exception('Failed to load data');
}
} on DioException catch (e) {
final String errorMessage;
if (e.response?.data != null) {
errorMessage = e.response?.data['message'] ?? 'Something went wrong';
} else {
errorMessage = 'Something went wrong';
}
throw (errorMessage);
}
}
// logout
Future<LoginResponse> logout() async {
final SharedPreferences pref = await SharedPreferences.getInstance();
final String auth = 'Bearer ${pref.getString('token')}';
try {
final response = await _dioWithoutInterceptor.post(
'/auth/logout',
options: Options(
headers: {'Authorization': auth},
),
);
if (response.statusCode == 200) {
final data = LoginResponse.fromJson(response.data);
pref.remove('token');
pref.remove('jwtToken');
pref.remove('username');
pref.remove('email');
pref.remove('fullName');
pref.setBool('isLoggedIn', false);
return data;
} else {
throw Exception('Failed to load data');
}
} on DioException catch (e) {
final String errorMessage = e.response?.data['message'];
throw (errorMessage);
}
}
Future<RelayResponse> getRelayStatus() async {
final SharedPreferences pref = await SharedPreferences.getInstance();
final String auth = 'Bearer ${pref.getString('token')}';
try {
await Future.delayed(const Duration(seconds: 3));
final result = await _dioWithoutInterceptor.get('get-relay');
final result = await _dioWithoutInterceptor.get(
'/api/get-relay',
options: Options(
headers: {'Authorization': auth},
),
);
if (result.statusCode == 200) {
final data = RelayResponse.fromJson(result.data);
return data;
@ -21,10 +120,144 @@ class AppService {
throw Exception('Failed to load data');
}
} on DioException catch (e) {
if (kDebugMode) {
print(e);
final errorMessage = e.response?.data['message'];
throw (errorMessage);
}
rethrow;
}
// switch relay
Future<SwitchRelayResponse> switchRelay(
{required int relayNumber, required int state}) async {
final SharedPreferences pref = await SharedPreferences.getInstance();
final String auth = 'Bearer ${pref.getString('token')}';
try {
final response = await _dioWithoutInterceptor.post(
'/api/set-relay',
data: {
'id': relayNumber,
'state': state,
},
options: Options(
headers: {'Authorization': auth},
),
);
if (response.statusCode == 200 || response.statusCode == 201) {
final data = SwitchRelayResponse.fromJson(response.data);
return data;
} else {
throw Exception('Failed to load data');
}
} on DioException catch (e) {
final errorMessage = e.response?.data['message'];
throw (errorMessage);
}
}
//get grafik data dht
Future<DhtGraphicResponse> getGrafikDataDht({
required String metric,
}) async {
final SharedPreferences pref = await SharedPreferences.getInstance();
final String auth = 'Bearer ${pref.getString('token')}';
String date = DateTime.now().toString();
final formatedDate = dateFormatterShort(date);
try {
final result = await _dioWithoutInterceptor.get(
'/api/sensor/getData?metric=$metric&range[start]=$formatedDate&range[end]=$formatedDate&range[time_range]=HOURLY&sensor=dht',
options: Options(
headers: {'Authorization': auth},
),
);
if (result.statusCode == 200) {
final data = DhtGraphicResponse.fromJson(result.data);
return data;
} else {
throw Exception('Failed to load data');
}
} on DioException catch (e) {
final errorMessage = e.response?.data['message'];
throw (errorMessage);
}
}
// get grafik data npk1
Future<Npk1GraphicResponse> getGraphicDataNpk1(
{required String metric}) async {
final SharedPreferences pref = await SharedPreferences.getInstance();
final String auth = 'Bearer ${pref.getString('token')}';
String date = DateTime.now().toString();
final formatedDate = dateFormatterShort(date);
try {
final result = await _dioWithoutInterceptor.get(
'/api/sensor/getData?metric=$metric&range[start]=$formatedDate&range[end]=$formatedDate&range[time_range]=HOURLY&sensor=npk1',
options: Options(
headers: {'Authorization': auth},
),
);
if (result.statusCode == 200) {
final data = Npk1GraphicResponse.fromJson(result.data);
return data;
} else {
throw Exception('Failed to load data');
}
} on DioException catch (e) {
final errorMessage = e.response?.data['message'];
throw (errorMessage);
}
}
// get grafik data npk2
Future<Npk2GraphicResponse> getGraphicDataNpk2(
{required String metric}) async {
final SharedPreferences pref = await SharedPreferences.getInstance();
final String auth = 'Bearer ${pref.getString('token')}';
String date = DateTime.now().toString();
final formatedDate = dateFormatterShort(date);
try {
final result = await _dioWithoutInterceptor.get(
'/api/sensor/getData?metric=$metric&range[start]=$formatedDate&range[end]=$formatedDate&range[time_range]=HOURLY&sensor=npk2',
options: Options(
headers: {'Authorization': auth},
),
);
if (result.statusCode == 200) {
final data = Npk2GraphicResponse.fromJson(result.data);
return data;
} else {
throw Exception('Failed to load data');
}
} on DioException catch (e) {
final errorMessage = e.response?.data['message'];
throw (errorMessage);
}
}
// get latest data
Future<LatestDataResponse> getLatestData() async {
final SharedPreferences pref = await SharedPreferences.getInstance();
final String auth = 'Bearer ${pref.getString('token')}';
try {
final result = await _dioWithoutInterceptor.get(
'/api/sensor/getLatest',
options: Options(
headers: {'Authorization': auth},
),
);
if (result.statusCode == 200) {
final data = LatestDataResponse.fromJson(result.data);
return data;
} else {
throw Exception('Failed to load data');
}
} on DioException catch (e) {
final errorMessage = e.response?.data['message'];
throw (errorMessage);
}
}
}

View File

@ -0,0 +1,20 @@
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
class FirebaseApi {
final _firebaseMessaging = FirebaseMessaging.instance;
Future<void> initNotification() async {
await _firebaseMessaging.requestPermission(
alert: true,
badge: true,
provisional: false,
sound: true,
);
final fCMToken = await _firebaseMessaging.getToken();
if (kDebugMode) {
print('FCM Token: $fCMToken');
}
}
}

View File

@ -1,170 +1,170 @@
import 'dart:convert';
// import 'dart:convert';
import 'package:agrilink_vocpro/core/constant/app_constant.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:mqtt_client/mqtt_client.dart';
import 'package:mqtt_client/mqtt_server_client.dart';
// import 'package:agrilink_vocpro/core/constant/app_constant.dart';
// import 'package:agrilink_vocpro/core/state/result_state.dart';
// import 'package:mqtt_client/mqtt_client.dart';
// import 'package:mqtt_client/mqtt_server_client.dart';
class MQTTService {
MqttServerClient? client;
// class MQTTService {
// MqttServerClient? client;
Future<ResultState> setupMqtt() async {
client = MqttServerClient(AppConstant.mqttServer, '');
client!.port = 1883;
// Future<ResultState> setupMqtt() async {
// client = MqttServerClient(AppConstant.mqttServer, '');
// client!.port = 1883;
client!.connectionMessage = MqttConnectMessage()
.authenticateAs(AppConstant.mqttUsername, AppConstant.mqttPassword)
.withClientIdentifier('mobile_client_controller')
.startClean() // reset session
.withWillQos(MqttQos.atLeastOnce);
// client!.connectionMessage = MqttConnectMessage()
// .authenticateAs(AppConstant.mqttUsername, AppConstant.mqttPassword)
// .withClientIdentifier('mobile_client_controller')
// .startClean() // reset session
// .withWillQos(MqttQos.atLeastOnce);
try {
print('MQTT: Connecting....');
await client!.connect();
print('MQTT: Connected');
return ResultState.hasData;
} catch (e) {
print('MQTT: Error: $e');
return ResultState.error;
}
}
// try {
// print('MQTT: Connecting....');
// await client!.connect();
// print('MQTT: Connected');
// return ResultState.hasData;
// } catch (e) {
// print('MQTT: Error: $e');
// return ResultState.error;
// }
// }
Future<ResultState> publishMessage(String topic, String message) async {
final builder = MqttClientPayloadBuilder();
// Future<ResultState> publishMessage(String topic, String message) async {
// final builder = MqttClientPayloadBuilder();
try {
final bool isConnected = await isMqttConnected(); // Cek apakah terhubung
if (!isConnected) {
print('MQTT: Tidak terhubung ke broker. Tidak bisa publish message.');
return ResultState.error;
}
// try {
// final bool isConnected = await isMqttConnected(); // Cek apakah terhubung
// if (!isConnected) {
// print('MQTT: Tidak terhubung ke broker. Tidak bisa publish message.');
// return ResultState.error;
// }
print('MQTT: Published message to $topic: $message');
builder.addString(message);
client!.publishMessage(topic, MqttQos.atMostOnce, builder.payload!);
print('MQTT: Message published');
return ResultState.hasData;
} catch (e) {
print('MQTT: Error: $e');
return ResultState.error;
}
}
// print('MQTT: Published message to $topic: $message');
// builder.addString(message);
// client!.publishMessage(topic, MqttQos.atMostOnce, builder.payload!);
// print('MQTT: Message published');
// return ResultState.hasData;
// } catch (e) {
// print('MQTT: Error: $e');
// return ResultState.error;
// }
// }
Future<ResultState> disconnectMqtt() async {
final bool isConnected = await isMqttConnected();
if (isConnected) {
print('Memutus koneksi dari broker...');
// Future<ResultState> disconnectMqtt() async {
// final bool isConnected = await isMqttConnected();
// if (isConnected) {
// print('Memutus koneksi dari broker...');
client!.disconnect();
// client!.disconnect();
await Future.delayed(const Duration(seconds: 1));
print('Koneksi telah terputus.');
return ResultState.hasData;
} else {
print('Tidak ada koneksi yang sedang aktif.');
return ResultState.error;
}
}
// await Future.delayed(const Duration(seconds: 1));
// print('Koneksi telah terputus.');
// return ResultState.hasData;
// } else {
// print('Tidak ada koneksi yang sedang aktif.');
// return ResultState.error;
// }
// }
Future<bool> isMqttConnected() async {
if (client != null &&
client!.connectionStatus!.state == MqttConnectionState.connected) {
return true; //connected
} else {
return false; //not connected
}
}
// Future<bool> isMqttConnected() async {
// if (client != null &&
// client!.connectionStatus!.state == MqttConnectionState.connected) {
// return true; //connected
// } else {
// return false; //not connected
// }
// }
Future<bool> subscribeToTopic(String topic) async {
bool isActive = false;
if (client != null &&
client!.connectionStatus!.state == MqttConnectionState.connected) {
try {
print('MQTT: Subscribing to $topic');
client!.subscribe(topic, MqttQos.atMostOnce);
print('MQTT: Subscribed to $topic');
// Future<bool> subscribeToTopic(String topic) async {
// bool isActive = false;
// if (client != null &&
// client!.connectionStatus!.state == MqttConnectionState.connected) {
// try {
// print('MQTT: Subscribing to $topic');
// client!.subscribe(topic, MqttQos.atMostOnce);
// print('MQTT: Subscribed to $topic');
// Tambahkan log ini untuk memastikan bahwa listener dijalankan
if (client!.updates != null) {
print('MQTT: Listening for updates...');
} else {
print('MQTT: No updates stream available');
}
// // Tambahkan log ini untuk memastikan bahwa listener dijalankan
// if (client!.updates != null) {
// print('MQTT: Listening for updates...');
// } else {
// print('MQTT: No updates stream available');
// }
client!.updates!.listen(
(List<MqttReceivedMessage<MqttMessage?>>? messages) {
print('MQTT: Subscribe Message received!');
if (messages != null && messages.isNotEmpty) {
final MqttPublishMessage recMessage =
messages[0].payload as MqttPublishMessage;
final String payload = MqttPublishPayload.bytesToStringAsString(
recMessage.payload.message);
print(
'MQTT: Subscribe Message received on topic ${messages[0].topic}: $payload');
// client!.updates!.listen(
// (List<MqttReceivedMessage<MqttMessage?>>? messages) {
// print('MQTT: Subscribe Message received!');
// if (messages != null && messages.isNotEmpty) {
// final MqttPublishMessage recMessage =
// messages[0].payload as MqttPublishMessage;
// final String payload = MqttPublishPayload.bytesToStringAsString(
// recMessage.payload.message);
// print(
// 'MQTT: Subscribe Message received on topic ${messages[0].topic}: $payload');
if (payload == 'ON') {
isActive = true;
// Update UI atau provider untuk menandakan relay ON
} else if (payload == 'OFF') {
isActive = false;
// Update UI atau provider untuk menandakan relay OFF
} else {
print('MQTT: Invalid Subscribe message received');
}
} else {
print('MQTT: No Subscribe messages received');
}
},
);
// if (payload == 'ON') {
// isActive = true;
// // Update UI atau provider untuk menandakan relay ON
// } else if (payload == 'OFF') {
// isActive = false;
// // Update UI atau provider untuk menandakan relay OFF
// } else {
// print('MQTT: Invalid Subscribe message received');
// }
// } else {
// print('MQTT: No Subscribe messages received');
// }
// },
// );
return isActive;
} catch (e) {
print('MQTT: Error subscribing to $topic: $e');
return isActive;
}
} else {
print('MQTT: Not connected, cannot subscribe.');
return false;
}
}
// return isActive;
// } catch (e) {
// print('MQTT: Error subscribing to $topic: $e');
// return isActive;
// }
// } else {
// print('MQTT: Not connected, cannot subscribe.');
// return false;
// }
// }
Future<ResultState> subscribeToRelayStatus() async {
if (client != null &&
client!.connectionStatus!.state == MqttConnectionState.connected) {
try {
print('MQTT: Subscribing to /smartfarming/getRelayStatus');
client!.subscribe('smartfarming/getRelayStatus', MqttQos.atMostOnce);
print('MQTT: Subscribed to /smartfarming/getRelayStatus');
// Future<ResultState> subscribeToRelayStatus() async {
// if (client != null &&
// client!.connectionStatus!.state == MqttConnectionState.connected) {
// try {
// print('MQTT: Subscribing to /smartfarming/getRelayStatus');
// client!.subscribe('smartfarming/getRelayStatus', MqttQos.atMostOnce);
// print('MQTT: Subscribed to /smartfarming/getRelayStatus');
client!.updates!
.listen((List<MqttReceivedMessage<MqttMessage?>>? messages) {
if (messages != null && messages.isNotEmpty) {
final MqttPublishMessage recMessage =
messages[0].payload as MqttPublishMessage;
final String payload = MqttPublishPayload.bytesToStringAsString(
recMessage.payload.message);
print(
'MQTT: Message received on topic ${messages[0].topic}: $payload');
// client!.updates!
// .listen((List<MqttReceivedMessage<MqttMessage?>>? messages) {
// if (messages != null && messages.isNotEmpty) {
// final MqttPublishMessage recMessage =
// messages[0].payload as MqttPublishMessage;
// final String payload = MqttPublishPayload.bytesToStringAsString(
// recMessage.payload.message);
// print(
// 'MQTT: Message received on topic ${messages[0].topic}: $payload');
// Parse the received JSON payload
final Map<String, dynamic> relayStatus = jsonDecode(payload);
// // Parse the received JSON payload
// final Map<String, dynamic> relayStatus = jsonDecode(payload);
print('MQTT: Relay status: $relayStatus');
// print('MQTT: Relay status: $relayStatus');
// Assuming you are using provider, notify it with new relay status
// _updateRelayStatus(relayStatus);
} else {
print('MQTT: No messages received');
}
});
return ResultState.hasData;
} catch (e) {
print('MQTT: Error subscribing: $e');
return ResultState.error;
}
} else {
print('MQTT: Not connected, cannot subscribe.');
return ResultState.error;
}
}
}
// // Assuming you are using provider, notify it with new relay status
// // _updateRelayStatus(relayStatus);
// } else {
// print('MQTT: No messages received');
// }
// });
// return ResultState.hasData;
// } catch (e) {
// print('MQTT: Error subscribing: $e');
// return ResultState.error;
// }
// } else {
// print('MQTT: Not connected, cannot subscribe.');
// return ResultState.error;
// }
// }
// }

View File

@ -1,12 +1,109 @@
import 'dart:convert';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:googleapis_auth/auth_io.dart';
import 'package:http/http.dart' as http;
class AuthProvider extends ChangeNotifier {
TextEditingController emailController = TextEditingController();
TextEditingController passwordController = TextEditingController();
bool _isRememberMe = false;
bool get isRememberMe => _isRememberMe;
String errorMessage = '';
ResultState loginState = ResultState.initial;
void controllerClear() {
emailController.clear();
passwordController.clear();
notifyListeners();
}
void setRememberMe(bool value) {
_isRememberMe = value;
notifyListeners();
}
Future<void> login(context,
{required String email, required String password}) async {
loginState = ResultState.loading;
notifyListeners();
try {
final result = await AppService().login(
username: email,
password: password,
rememberMe: isRememberMe.toString(),
);
if (result.data != null) {
loginState = ResultState.hasData;
notifyListeners();
} else {
errorMessage = 'Login gagal, data tidak ditemukan';
loginState = ResultState.error;
notifyListeners();
}
} catch (e) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Error'),
content: Text('$e'),
actions: [
TextButton(
child: const Text('OK'),
onPressed: () {
Navigator.of(context).pop();
},
),
],
);
},
);
loginState = ResultState.error;
notifyListeners();
}
}
bool validateInputs() {
if (emailController.text.isEmpty || passwordController.text.isEmpty) {
errorMessage = 'Email dan password tidak boleh kosong';
loginState = ResultState.error;
notifyListeners();
return false;
}
return true;
}
Future<void> getAccessToken() async {
try {
final serviceAccountJson = await rootBundle.loadString(
'assets/json/agrilink-vocpro-b37f9-firebase-adminsdk-a4f6g-e6565344e3.json',
);
final accountCredentials = ServiceAccountCredentials.fromJson(
json.decode(serviceAccountJson),
);
const scope = ['https://www.googleapis.com/auth/firebase.messaging'];
final client = http.Client();
try {
final accessCredential = await obtainAccessCredentialsViaServiceAccount(
accountCredentials, scope, client);
final accessToken = accessCredential.accessToken.data;
print('Access Token: $accessToken');
} catch (e) {
print('Error: $e');
}
} catch (e) {
print('Error 2: $e');
}
}
}

View File

@ -1,8 +1,9 @@
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/route/app_route.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/core/widgets/app_button.dart';
import 'package:agrilink_vocpro/core/widgets/app_textfield.dart';
import 'package:agrilink_vocpro/core/widgets/password_textfield.dart';
import 'package:agrilink_vocpro/features/auth/provider/auth_provider.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
@ -14,18 +15,15 @@ class LoginScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
body: GestureDetector(
onTap: () {
FocusScope.of(context).unfocus();
},
child: SafeArea(
child: Consumer<AuthProvider>(builder: (context, authP, child) {
body: SafeArea(
child: Consumer<AuthProvider>(
builder: (context, authP, child) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
const SizedBox(height: 40),
Text(
'Hello Wellcome back 👋',
'Hello, Welcome back 👋',
style: AppTheme.titleLarge,
),
Text(
@ -37,36 +35,56 @@ class LoginScreen extends StatelessWidget {
const SizedBox(height: 4),
AppTextfield(
controller: authP.emailController,
hintText: 'Masukkan username',
hintText: 'Masukkan email',
),
const SizedBox(height: 24),
Text('Password', style: AppTheme.labelLarge),
const SizedBox(height: 4),
AppTextfield(
PasswordTextfield(
controller: authP.passwordController,
hintText: 'Masukkan password',
),
const SizedBox(height: 24),
GestureDetector(
onTap: () {
print('Forgot password?');
Row(
children: [
Checkbox(
value: authP.isRememberMe,
onChanged: (value) {
authP.setRememberMe(value!);
},
child: Text(
'Forgot password?',
textAlign: TextAlign.end,
style: AppTheme.labelMedium
.copyWith(color: AppColor.secondary),
),
Text('Remember me', style: AppTheme.labelLarge),
],
),
const SizedBox(height: 24),
authP.loginState == ResultState.loading
? const Center(child: CircularProgressIndicator())
: AppButton(
onPressed: () async {
await authP.login(
context,
email: authP.emailController.text,
password: authP.passwordController.text,
);
if (context.mounted) {
if (authP.loginState == ResultState.hasData) {
context.go(AppRoute.dashboard);
authP.controllerClear();
authP.loginState = ResultState.initial;
}
}
},
text: 'Login',
),
SizedBox(height: 24),
AppButton(
onPressed: () {
GoRouter.of(context).go(AppRoute.dashboard);
authP.getAccessToken();
},
text: 'Login'),
text: 'Get Access Token'),
],
);
}),
},
),
),
);

View File

@ -1,8 +1,6 @@
import 'dart:math';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/material.dart';
import 'package:flutter/foundation.dart';
class ControlProvider extends ChangeNotifier {
final AppService _appService = AppService();
@ -54,6 +52,9 @@ class ControlProvider extends ChangeNotifier {
relayState = ResultState.loading;
notifyListeners();
try {
if (kDebugMode) {
print('try to get relay status...');
}
final result = await _appService.getRelayStatus();
if (result.success == true) {
for (var element in result.data!) {
@ -73,26 +74,44 @@ class ControlProvider extends ChangeNotifier {
} catch (e) {
relayState = ResultState.error;
notifyListeners();
if (kDebugMode) {
print(e);
rethrow;
}
}
}
// Future<void> disconnectMqtt() async {
// try {
// await _mqttService.disconnectMqtt();
// } catch (e) {
// print(e);
// rethrow;
// }
// notifyListeners();
// }
Future<bool> switchRelay(int relayNumber, bool state) async {
relayState = ResultState.loading;
notifyListeners();
// @override
// void dispose() {
// disconnectMqtt();
// super.dispose();
// }
final int stateConverted;
if (state == true) {
stateConverted = 1;
} else {
stateConverted = 0;
}
try {
final result = await _appService.switchRelay(
relayNumber: relayNumber, state: stateConverted);
if (result.success == true) {
relayState = ResultState.hasData;
notifyListeners();
if (kDebugMode) {
print(result.message);
}
return true;
} else {
return false;
}
} catch (e) {
if (kDebugMode) {
print(e);
}
relayState = ResultState.error;
notifyListeners();
return false;
}
}
void switchControl1(bool value) {
_control_1 = value;

View File

@ -1,7 +1,7 @@
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/control/provider/control_provider.dart';
import 'package:agrilink_vocpro/features/control/widgets/control_button_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
@ -22,11 +22,43 @@ class ControlScreen extends StatelessWidget {
scrolledUnderElevation: 0,
),
body: RefreshIndicator(
displacement: 10,
onRefresh: () async =>
await context.read<ControlProvider>().getRelayStatus(),
child: SafeArea(
child: ListView(
children: [
Consumer<ControlProvider>(builder: (context, provider, child) {
switch (provider.relayState) {
case ResultState.loading:
return const StatusBarWidget(
text: 'Memuat...',
icon: BootstrapIcons.cloud,
color: Colors.cyan,
isLoading: true,
);
case ResultState.hasData:
return const StatusBarWidget(
text: 'Berhasil terhubung',
icon: BootstrapIcons.check_circle,
color: Colors.teal,
);
case ResultState.noData:
return const StatusBarWidget(
text: 'Data tidak ditemukan',
icon: BootstrapIcons.exclamation_circle,
color: Colors.amber,
);
case ResultState.initial:
return const SizedBox.shrink();
case ResultState.error:
return const StatusBarWidget(
text: 'Tidak dapat terhubung',
icon: BootstrapIcons.exclamation_circle,
color: Colors.red,
);
}
}),
SizedBox(height: 16.h),
GridView(
padding: EdgeInsets.all(16.r),
@ -36,98 +68,82 @@ class ControlScreen extends StatelessWidget {
crossAxisCount: 2,
crossAxisSpacing: 16.r,
mainAxisSpacing: 16.r,
childAspectRatio: 1.4.h,
childAspectRatio: 1.35.h,
),
children: [
ControlButtonWidget(
title: 'Katup Air',
subtitle: 'Relay 1',
isActive: provider.control_1,
onTap: () {},
onTap: () async {
final result = provider.control_1 != true
? await provider.switchRelay(1, true)
: await provider.switchRelay(1, false);
result == true
? provider.switchControl1(!provider.control_1)
: provider.switchControl1(provider.control_1);
},
),
ControlButtonWidget(
title: 'Lampu Utama',
subtitle: 'Relay 2',
isActive: provider.control_2,
onTap: () {},
),
]),
],
),
),
),
);
}
}
class ControlButtonWidget extends StatelessWidget {
const ControlButtonWidget({
super.key,
required this.title,
required this.subtitle,
required this.isActive,
required this.onTap,
});
final String title;
final String subtitle;
final bool isActive;
final Function() onTap;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: Colors.grey.withOpacity(0.2),
spreadRadius: 1.r,
blurRadius: 16.r,
offset: Offset(0, 12.r),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: AppTheme.labelMedium),
Text(subtitle, style: AppTheme.labelSmall),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Consumer<ControlProvider>(
builder: (context, provider, child) {
switch (provider.relayState) {
case ResultState.loading:
return CircleAvatar(
radius: 20.r,
backgroundColor: Colors.transparent,
child: const CupertinoActivityIndicator(),
);
default:
return InkWell(
highlightColor: Colors.black,
onTap: onTap,
child: CircleAvatar(
radius: 20.r,
backgroundColor: isActive
? AppColor.secondary
: Colors.grey.shade400,
child: const Icon(
BootstrapIcons.power,
color: Colors.white,
),
),
);
}
onTap: () {
provider.control_2 != true
? provider.switchControl2(true)
: provider.switchControl2(false);
},
),
],
)
),
],
),
),
),
);
}
}
class StatusBarWidget extends StatelessWidget {
const StatusBarWidget({
super.key,
required this.text,
required this.icon,
required this.color,
this.isLoading = false,
});
final String text;
final IconData icon;
final Color color;
final bool isLoading;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.symmetric(vertical: 8.h, horizontal: 16.w),
margin: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: color.withAlpha(50),
borderRadius: BorderRadius.circular(8.r),
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Icon(
icon,
color: color,
size: 16.r,
),
SizedBox(width: 8.w),
Text(text, style: AppTheme.titleSmall.copyWith(color: color)),
const Spacer(),
if (isLoading)
Center(
child: CupertinoActivityIndicator(
radius: 8.r,
))
else
const SizedBox.shrink(),
],
),
);

View File

@ -0,0 +1,85 @@
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/control/provider/control_provider.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
class ControlButtonWidget extends StatelessWidget {
const ControlButtonWidget({
super.key,
required this.title,
required this.subtitle,
required this.isActive,
required this.onTap,
});
final String title;
final String subtitle;
final bool isActive;
final Function() onTap;
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(16.r),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.r),
boxShadow: [
BoxShadow(
color: isActive
? AppColor.secondary.withOpacity(0.2)
: Colors.grey.withOpacity(0.2),
spreadRadius: 1.r,
blurRadius: 16.r,
offset: Offset(0, 12.r),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title, style: AppTheme.labelMedium),
Text(subtitle, style: AppTheme.labelSmall),
const Spacer(),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Consumer<ControlProvider>(
builder: (context, provider, child) {
switch (provider.relayState) {
case ResultState.loading:
return CircleAvatar(
radius: 20.r,
backgroundColor: Colors.transparent,
child: const CupertinoActivityIndicator(),
);
default:
return InkWell(
highlightColor: Colors.black,
onTap: onTap,
child: CircleAvatar(
radius: 20.r,
backgroundColor: isActive
? AppColor.secondary
: Colors.grey.shade400,
child: const Icon(
BootstrapIcons.power,
color: Colors.white,
),
),
);
}
},
),
],
)
],
),
);
}
}

View File

@ -0,0 +1,57 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class ConductivityProvider extends ChangeNotifier {
ConductivityProvider() {
getSoilConductivityNpk1Data();
getSoilConductivityNpk2Data();
}
ResultState dataState = ResultState.initial;
List<Npk> dataFetchedNpk1 = [];
List<Npk> dataFetchedNpk2 = [];
Future<void> getSoilConductivityNpk1Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGraphicDataNpk1(metric: 'soilConductivity');
if (result.data == null || result.data!.npk1!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk1 = result.data!.npk1 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
Future<void> getSoilConductivityNpk2Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGraphicDataNpk2(metric: 'soilConductivity');
if (result.data == null || result.data!.npk2!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk2 = result.data!.npk2 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -1,20 +1,30 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/conductivity/provider/conductivity_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_error_widget.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
class ConductivityScreen extends StatelessWidget {
const ConductivityScreen({super.key, this.conductivity = 0.0});
const ConductivityScreen(
{super.key, this.conductivityNpk1 = 0.0, this.conductivityNpk2 = 0.0});
final double conductivity;
double get value => conductivity;
final double conductivityNpk1;
final double conductivityNpk2;
@override
Widget build(BuildContext context) {
return Scaffold(
return ChangeNotifierProvider(
create: (context) => ConductivityProvider(),
child: DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text('Conductivity', style: AppTheme.labelMedium),
title: Text('Soil Conductivity', style: AppTheme.labelMedium),
centerTitle: true,
backgroundColor: Colors.white,
scrolledUnderElevation: 0,
@ -27,13 +37,46 @@ class ConductivityScreen extends StatelessWidget {
),
)
],
bottom: const TabBar(
tabs: [
Tab(text: 'NPK 1'),
Tab(text: 'NPK 2'),
],
),
body: Center(
),
body: TabBarView(
children: [
buildTabContent(context, conductivityNpk1, 'NPK 1', true),
buildTabContent(context, conductivityNpk2, 'NPK 2', false),
],
),
),
),
);
}
SafeArea buildTabContent(
BuildContext context, double value, String label, bool isNpk1) {
return SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(height: 32.h),
Column(
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
buildSoilInfo(context, value),
SizedBox(height: 16.h),
buildInfoRow(context),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
buildGraphicContent(context, isNpk1),
],
),
);
}
Widget buildSoilInfo(BuildContext context, double value) {
return Center(
child: Column(
children: [
Icon(
Icons.electric_bolt_rounded,
@ -43,12 +86,15 @@ class ConductivityScreen extends StatelessWidget {
Text('$value µS/cm', style: AppTheme.headline1),
],
),
SizedBox(height: 32.h),
Row(
);
}
Widget buildInfoRow(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Daya Arus Listrik',
'Soil Condutivity',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
@ -56,13 +102,14 @@ class ConductivityScreen extends StatelessWidget {
iconSize: 20.r,
color: Colors.blue,
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle))
],
icon: const Icon(BootstrapIcons.info_circle),
),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
],
);
}
Widget buildGraphicContent(BuildContext context, bool isNpk1) {
return AspectRatio(
aspectRatio: 1.6.h,
child: Container(
decoration: BoxDecoration(
@ -70,15 +117,43 @@ class ConductivityScreen extends StatelessWidget {
borderRadius: BorderRadius.circular(16.w),
border: Border.all(color: Colors.grey.shade300, width: 1.w),
),
child: const GarphicWidget(
gradientColors: [
Colors.cyan,
Colors.teal,
],
child: Consumer<ConductivityProvider>(
builder: (context, provider, child) {
final dataState = provider.dataState;
switch (dataState) {
case ResultState.loading:
return const Center(child: CupertinoActivityIndicator());
case ResultState.hasData:
return GarphicWidget(
gradientColors: const [Colors.teal, Colors.greenAccent],
hour: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].hour ?? 0
: provider.dataFetchedNpk2[index].hour ?? 0,
),
data: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].soilconductivity ?? 0
: provider.dataFetchedNpk2[index].soilconductivity ?? 0,
),
)
],
maxValue: 1,
);
case ResultState.error:
return const GraphicErrorWidget(message: 'Terjadi Kesalahan');
case ResultState.noData:
return const GraphicErrorWidget(message: 'Tidak Ada Data');
case ResultState.initial:
default:
return const SizedBox.shrink();
}
},
),
),
);

View File

@ -0,0 +1,34 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class HumidityProvider extends ChangeNotifier {
HumidityProvider() {
getHumidityData();
}
ResultState dataState = ResultState.initial;
List<Dht> dataFetched = [];
Future<void> getHumidityData() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGrafikDataDht(metric: 'viciHumidity');
if (result.data == null || result.data!.dht!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetched = result.data!.dht ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Humidity Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -1,4 +1,6 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/humidity/provider/humidity_provider.dart';
import 'package:agrilink_vocpro/features/home/provider/home_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
@ -16,7 +18,9 @@ class HumidityScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
return ChangeNotifierProvider(
create: (context) => HumidityProvider(),
child: Scaffold(
appBar: AppBar(
title: Text('Humidity', style: AppTheme.labelMedium),
centerTitle: true,
@ -54,7 +58,7 @@ class HumidityScreen extends StatelessWidget {
children: [
const Icon(BootstrapIcons.droplet_half,
size: 32, color: Colors.blue),
Text('60 %', style: AppTheme.headline1),
Text('$humidity %', style: AppTheme.headline1),
],
),
),
@ -63,7 +67,7 @@ class HumidityScreen extends StatelessWidget {
child: AnimatedRadialGauge(
duration: const Duration(seconds: 3),
curve: Curves.easeOut,
value: 60,
value: humidity,
axis: GaugeAxis(
degrees: 360,
min: 0,
@ -114,14 +118,74 @@ class HumidityScreen extends StatelessWidget {
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.w),
border:
Border.all(color: Colors.grey.shade300, width: 1.w)),
child: GarphicWidget(
gradientColors: [
Colors.blue.shade200,
Colors.blue,
border: Border.all(
color: Colors.grey.shade300, width: 1.w)),
child: Consumer<HumidityProvider>(
builder: (context, provider, child) {
switch (provider.dataState) {
case ResultState.loading:
return const Center(
child: CupertinoActivityIndicator(),
);
case ResultState.hasData:
return GarphicWidget(
gradientColors: const [
Colors.cyan,
Colors.amber,
],
hour: List.generate(
provider.dataFetched.length,
(index) =>
provider.dataFetched[index].hour ?? 0),
data: List.generate(
provider.dataFetched.length,
(index) =>
provider.dataFetched[index].vicihumidity
?.toDouble() ??
0),
);
case ResultState.error:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.exclamation_circle,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Terjadi Kesalahan',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.noData:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.database_fill_x,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Tidak Ada Data',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.initial:
return const SizedBox.shrink();
default:
return const Center(
child: Text('Default Error'),
);
}
}),
),
),
SizedBox(height: 16.h),
@ -129,41 +193,11 @@ class HumidityScreen extends StatelessWidget {
padding: EdgeInsets.only(left: 16.w),
child: const Text('Deskripsi'),
),
// ListView.builder(
// shrinkWrap: true,
// physics: const NeverScrollableScrollPhysics(),
// itemCount: provider.humidtyRules.length,
// itemBuilder: (context, index) {
// final item = provider.humidtyRules[index];
// return Theme(
// data: Theme.of(context)
// .copyWith(dividerColor: Colors.transparent),
// child: ExpansionTile(
// trailing: Text(
// item.censorText,
// style: TextStyle(color: item.color),
// ),
// expandedCrossAxisAlignment: CrossAxisAlignment.start,
// childrenPadding: EdgeInsets.all(16.r),
// title: Text(
// 'Kelembaban ${item.minPercentage}% - ${item.maxPercentage}%'),
// children: [
// Text(
// item.description,
// style: AppTheme.labelMedium,
// ),
// SizedBox(height: 8.h),
// Text('Tindakan', style: AppTheme.labelSmall),
// SizedBox(height: 8.h),
// Text(item.action),
// ],
// ),
// );
// })
],
);
}),
),
),
);
}
}

View File

@ -1,224 +0,0 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:gauge_indicator/gauge_indicator.dart';
class LightScreen extends StatelessWidget {
const LightScreen({super.key, this.lightIntensity = 0});
final double lightIntensity;
double get value => lightIntensity;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Light', style: AppTheme.labelMedium),
centerTitle: true,
backgroundColor: Colors.white,
scrolledUnderElevation: 0,
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Icon(
BootstrapIcons.sun,
color: Colors.yellow.shade600,
),
)
],
),
body: SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.05,
),
SizedBox(
height: 240.h,
child: Stack(
fit: StackFit.expand,
children: [
const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(BootstrapIcons.sun,
size: 32, color: Colors.orange),
],
),
),
AnimatedRadialGauge(
duration: const Duration(seconds: 3),
curve: Curves.easeOut,
value: value,
axis: GaugeAxis(
degrees: 360,
min: 0,
max: 1000,
style: GaugeAxisStyle(
background: Colors.grey.shade100,
thickness: 100,
),
progressBar: GaugeBasicProgressBar(
gradient: GaugeAxisGradient(colors: [
Colors.yellow.shade100,
Colors.orange.shade200,
]),
),
),
),
],
),
),
const SizedBox(height: 16),
Text(
'${value.toStringAsFixed(0)} lux',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Intensitas Cahaya',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
IconButton(
iconSize: 20.r,
color: Colors.blue,
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle))
],
),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
aspectRatio: 1.8.h,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300, width: 1.w),
borderRadius: BorderRadius.circular(16.w),
),
child: GarphicWidget(
gradientColors: [
Colors.yellow.shade100,
Colors.orange.shade200,
],
),
),
)
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceAround,
// children: [
// Container(
// height: 100.h,
// width: 100.w,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(16),
// color: Colors.blue.withOpacity(0.1),
// border: Border.all(
// color: Colors.blue,
// width: 2,
// ),
// ),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text('Low',
// style: AppTheme.labelMedium
// .copyWith(color: Colors.blue)),
// // SizedBox(height: 8.h),
// // const Icon(
// // BootstrapIcons.thermometer_low,
// // color: Colors.blue,
// // ),
// SizedBox(height: 8.h),
// Text(
// '<20°C',
// style: AppTheme.labelMedium,
// textAlign: TextAlign.center,
// ),
// ],
// ),
// ),
// Container(
// height: 100.h,
// width: 100.w,
// decoration: BoxDecoration(
// color: Colors.green.withOpacity(0.1),
// borderRadius: BorderRadius.circular(16),
// border: Border.all(
// color: Colors.green,
// width: 2,
// ),
// ),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text('Ideal',
// style: AppTheme.labelMedium
// .copyWith(color: Colors.green)),
// // SizedBox(height: 8.h),
// // const Icon(
// // BootstrapIcons.thermometer_half,
// // color: Colors.green,
// // ),
// SizedBox(height: 8.h),
// Text(
// '20-30°C',
// style: AppTheme.labelMedium,
// textAlign: TextAlign.center,
// ),
// ],
// ),
// ),
// Container(
// height: 100.h,
// width: 100.w,
// decoration: BoxDecoration(
// color: Colors.orange.withOpacity(0.1),
// borderRadius: BorderRadius.circular(16),
// border: Border.all(
// color: Colors.orange.shade800,
// width: 2,
// ),
// ),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text('high',
// style: AppTheme.labelMedium
// .copyWith(color: Colors.orange)),
// // SizedBox(height: 8.h),
// // const Icon(
// // BootstrapIcons.thermometer_high,
// // color: Colors.orange,
// // ),
// SizedBox(height: 8.h),
// Text(
// '>30°C',
// style: AppTheme.labelMedium,
// textAlign: TextAlign.center,
// ),
// ],
// ),
// ),
// ],
// )
],
),
),
);
}
}

View File

@ -0,0 +1,34 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class LumProvider extends ChangeNotifier {
LumProvider() {
getLumData();
}
ResultState dataState = ResultState.initial;
List<Dht> dataFetched = [];
Future<void> getLumData() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGrafikDataDht(metric: 'viciluminosity');
if (result.data == null || result.data!.dht!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetched = result.data!.dht ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Luminosity Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -0,0 +1,191 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/luminosity/provider/lum_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:gauge_indicator/gauge_indicator.dart';
import 'package:provider/provider.dart';
class LightScreen extends StatelessWidget {
const LightScreen({super.key, this.lightIntensity = 0});
final double lightIntensity;
double get value => lightIntensity;
@override
Widget build(BuildContext context) {
return ChangeNotifierProvider(
create: (context) => LumProvider(),
child: Scaffold(
appBar: AppBar(
title: Text('Light', style: AppTheme.labelMedium),
centerTitle: true,
backgroundColor: Colors.white,
scrolledUnderElevation: 0,
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Icon(
BootstrapIcons.sun,
color: Colors.yellow.shade600,
),
)
],
),
body: SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.05,
),
SizedBox(
height: 240.h,
child: Stack(
fit: StackFit.expand,
children: [
const Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Icon(BootstrapIcons.sun,
size: 32, color: Colors.orange),
],
),
),
AnimatedRadialGauge(
duration: const Duration(seconds: 3),
curve: Curves.easeOut,
value: value,
axis: GaugeAxis(
degrees: 360,
min: 0,
max: 1000,
style: GaugeAxisStyle(
background: Colors.grey.shade100,
thickness: 100,
),
progressBar: GaugeBasicProgressBar(
gradient: GaugeAxisGradient(colors: [
Colors.yellow.shade100,
Colors.orange.shade200,
]),
),
),
),
],
),
),
const SizedBox(height: 16),
Text(
'${value.toStringAsFixed(0)} lux',
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Intensitas Cahaya',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
IconButton(
iconSize: 20.r,
color: Colors.blue,
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle))
],
),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
aspectRatio: 1.8.h,
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.grey.shade300, width: 1.w),
borderRadius: BorderRadius.circular(16.w),
),
child: Consumer<LumProvider>(
builder: (context, provider, child) {
switch (provider.dataState) {
case ResultState.loading:
return const Center(
child: CupertinoActivityIndicator(),
);
case ResultState.hasData:
return GarphicWidget(
gradientColors: [
Colors.yellow.shade100,
Colors.orange.shade200,
],
hour: List.generate(provider.dataFetched.length,
(index) => provider.dataFetched[index].hour ?? 0),
data: List.generate(
provider.dataFetched.length,
(index) =>
provider.dataFetched[index].vicihumidity
?.toDouble() ??
0),
);
case ResultState.error:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.exclamation_circle,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Terjadi Kesalahan',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.noData:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.database_fill_x,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Tidak Ada Data',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.initial:
return const SizedBox.shrink();
default:
return const Center(
child: Text('Default Error'),
);
}
}),
),
)
],
),
),
),
);
}
}

View File

@ -0,0 +1,57 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class NitrogenProvider extends ChangeNotifier {
NitrogenProvider() {
getSoilNitrogenNpk1Data();
getSoilNitrogenNpk2Data();
}
ResultState dataState = ResultState.initial;
List<Npk> dataFetchedNpk1 = [];
List<Npk> dataFetchedNpk2 = [];
Future<void> getSoilNitrogenNpk1Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGraphicDataNpk1(metric: 'soilNitrogen');
if (result.data == null || result.data!.npk1!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk1 = result.data!.npk1 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
Future<void> getSoilNitrogenNpk2Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGraphicDataNpk2(metric: 'soilNitrogen');
if (result.data == null || result.data!.npk2!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk2 = result.data!.npk2 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -1,21 +1,30 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/nitrogen/provider/nitrogen_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_error_widget.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
class NitrogenScreen extends StatelessWidget {
const NitrogenScreen({super.key, this.nitrogen = 0.0});
const NitrogenScreen(
{super.key, this.nitrogenNpk1 = 0.0, this.nitrogenNpk2 = 0.0});
final double nitrogen;
double get value => nitrogen;
final double nitrogenNpk1;
final double nitrogenNpk2;
@override
Widget build(BuildContext context) {
return Scaffold(
return ChangeNotifierProvider(
create: (context) => NitrogenProvider(),
child: DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text('Nitrogen', style: AppTheme.labelMedium),
title: Text('Soil Nitrogen', style: AppTheme.labelMedium),
centerTitle: true,
backgroundColor: Colors.white,
scrolledUnderElevation: 0,
@ -23,18 +32,51 @@ class NitrogenScreen extends StatelessWidget {
Padding(
padding: EdgeInsets.only(right: 16),
child: Icon(
CupertinoIcons.eyedropper,
BootstrapIcons.eyedropper,
color: Colors.blue,
),
)
],
bottom: const TabBar(
tabs: [
Tab(text: 'NPK 1'),
Tab(text: 'NPK 2'),
],
),
body: Center(
),
body: TabBarView(
children: [
buildTabContent(context, nitrogenNpk1, 'NPK 1', true),
buildTabContent(context, nitrogenNpk2, 'NPK 2', false),
],
),
),
),
);
}
SafeArea buildTabContent(
BuildContext context, double value, String label, bool isNpk1) {
return SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(height: 32.h),
Column(
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
buildSoilInfo(context, value),
SizedBox(height: 16.h),
buildInfoRow(context),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
buildGraphicContent(context, isNpk1),
],
),
);
}
Widget buildSoilInfo(BuildContext context, double value) {
return Center(
child: Column(
children: [
Icon(
CupertinoIcons.eyedropper,
@ -44,12 +86,15 @@ class NitrogenScreen extends StatelessWidget {
Text('$value ppm', style: AppTheme.headline1),
],
),
SizedBox(height: 32.h),
Row(
);
}
Widget buildInfoRow(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Nitrogen',
'Soil Nitrogen',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
@ -57,13 +102,14 @@ class NitrogenScreen extends StatelessWidget {
iconSize: 20.r,
color: Colors.blue,
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle))
],
icon: const Icon(BootstrapIcons.info_circle),
),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
],
);
}
Widget buildGraphicContent(BuildContext context, bool isNpk1) {
return AspectRatio(
aspectRatio: 1.6.h,
child: Container(
decoration: BoxDecoration(
@ -71,15 +117,43 @@ class NitrogenScreen extends StatelessWidget {
borderRadius: BorderRadius.circular(16.w),
border: Border.all(color: Colors.grey.shade300, width: 1.w),
),
child: GarphicWidget(
gradientColors: [
Colors.blue.shade200,
Colors.blue,
],
child: Consumer<NitrogenProvider>(
builder: (context, provider, child) {
final dataState = provider.dataState;
switch (dataState) {
case ResultState.loading:
return const Center(child: CupertinoActivityIndicator());
case ResultState.hasData:
return GarphicWidget(
gradientColors: const [Colors.blue, Colors.blueAccent],
hour: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].hour ?? 0
: provider.dataFetchedNpk2[index].hour ?? 0,
),
data: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].soilnitrogen ?? 0
: provider.dataFetchedNpk2[index].soilnitrogen ?? 0,
),
)
],
maxValue: 1,
);
case ResultState.error:
return const GraphicErrorWidget(message: 'Terjadi Kesalahan');
case ResultState.noData:
return const GraphicErrorWidget(message: 'Tidak Ada Data');
case ResultState.initial:
default:
return const SizedBox.shrink();
}
},
),
),
);

View File

@ -0,0 +1,55 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class PhProvider extends ChangeNotifier {
PhProvider() {
getSoilPhNpk1Data();
getSoilPhNpk2Data();
}
ResultState dataState = ResultState.initial;
List<Npk> dataFetchedNpk1 = [];
List<Npk> dataFetchedNpk2 = [];
Future<void> getSoilPhNpk1Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result = await AppService().getGraphicDataNpk1(metric: 'soilPh');
if (result.data == null || result.data!.npk1!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk1 = result.data!.npk1 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
Future<void> getSoilPhNpk2Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result = await AppService().getGraphicDataNpk2(metric: 'soilPh');
if (result.data == null || result.data!.npk2!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk2 = result.data!.npk2 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -1,22 +1,30 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/ph/provider/ph_provider.dart';
import 'package:agrilink_vocpro/features/home/pages/ph/widget/ph_bar_pointer.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_error_widget.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
class PhScreen extends StatelessWidget {
const PhScreen({super.key, required this.phValue});
const PhScreen({super.key, this.phValueNpk1 = 0, this.phValueNpk2 = 0});
final double phValue;
double get value => phValue;
final double phValueNpk1;
final double phValueNpk2;
@override
Widget build(BuildContext context) {
return Scaffold(
return ChangeNotifierProvider(
create: (context) => PhProvider(),
child: DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text('pH Tanah', style: AppTheme.labelMedium),
title: Text('Soil Temperature', style: AppTheme.labelMedium),
centerTitle: true,
backgroundColor: Colors.white,
scrolledUnderElevation: 0,
@ -25,28 +33,59 @@ class PhScreen extends StatelessWidget {
padding: EdgeInsets.only(right: 16),
child: Icon(
BootstrapIcons.pie_chart,
color: Colors.orange,
color: Colors.amber,
),
)
],
bottom: const TabBar(
tabs: [
Tab(text: 'NPK 1'),
Tab(text: 'NPK 2'),
],
),
body: SafeArea(
),
body: TabBarView(
children: [
buildTabContent(context, phValueNpk1, 'NPK 1', true),
buildTabContent(context, phValueNpk2, 'NPK 2', false),
],
),
),
),
);
}
SafeArea buildTabContent(
BuildContext context, double value, String label, bool isNpk1) {
return SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.05,
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
buildSoilInfo(context, value),
SizedBox(height: 16.h),
buildInfoRow(context),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
buildGraphicContent(context, isNpk1),
],
),
Center(
child: PhIndicator(phValue: value), // Set nilai pH di sini
),
const SizedBox(height: 16),
const SizedBox(height: 16),
Row(
);
}
Widget buildSoilInfo(BuildContext context, double value) {
return Center(
child: PhIndicator(phValue: value),
);
}
Widget buildInfoRow(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'pH',
'Soil Acidity',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
@ -54,112 +93,14 @@ class PhScreen extends StatelessWidget {
iconSize: 20.r,
color: Colors.blue,
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle))
],
icon: const Icon(BootstrapIcons.info_circle),
),
SizedBox(height: 16.h),
// Row(
// mainAxisAlignment: MainAxisAlignment.spaceAround,
// children: [
// Container(
// height: 100.h,
// width: 100.w,
// decoration: BoxDecoration(
// borderRadius: BorderRadius.circular(16),
// color: Colors.blue.withOpacity(0.1),
// border: Border.all(
// color: Colors.blue,
// width: 2,
// ),
// ),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text('Low',
// style: AppTheme.labelMedium
// .copyWith(color: Colors.blue)),
// // SizedBox(height: 8.h),
// // const Icon(
// // BootstrapIcons.thermometer_low,
// // color: Colors.blue,
// // ),
// SizedBox(height: 8.h),
// Text(
// '<20°C',
// style: AppTheme.labelMedium,
// textAlign: TextAlign.center,
// ),
// ],
// ),
// ),
// Container(
// height: 100.h,
// width: 100.w,
// decoration: BoxDecoration(
// color: Colors.green.withOpacity(0.1),
// borderRadius: BorderRadius.circular(16),
// border: Border.all(
// color: Colors.green,
// width: 2,
// ),
// ),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text('Ideal',
// style: AppTheme.labelMedium
// .copyWith(color: Colors.green)),
// // SizedBox(height: 8.h),
// // const Icon(
// // BootstrapIcons.thermometer_half,
// // color: Colors.green,
// // ),
// SizedBox(height: 8.h),
// Text(
// '20-30°C',
// style: AppTheme.labelMedium,
// textAlign: TextAlign.center,
// ),
// ],
// ),
// ),
// Container(
// height: 100.h,
// width: 100.w,
// decoration: BoxDecoration(
// color: Colors.orange.withOpacity(0.1),
// borderRadius: BorderRadius.circular(16),
// border: Border.all(
// color: Colors.orange.shade800,
// width: 2,
// ),
// ),
// child: Column(
// mainAxisAlignment: MainAxisAlignment.center,
// children: [
// Text('high',
// style: AppTheme.labelMedium
// .copyWith(color: Colors.orange)),
// // SizedBox(height: 8.h),
// // const Icon(
// // BootstrapIcons.thermometer_high,
// // color: Colors.orange,
// // ),
// SizedBox(height: 8.h),
// Text(
// '>30°C',
// style: AppTheme.labelMedium,
// textAlign: TextAlign.center,
// ),
// ],
// ),
// ),
// ],
// ),
// SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
],
);
}
Widget buildGraphicContent(BuildContext context, bool isNpk1) {
return AspectRatio(
aspectRatio: 1.6.h,
child: Container(
decoration: BoxDecoration(
@ -167,15 +108,43 @@ class PhScreen extends StatelessWidget {
borderRadius: BorderRadius.circular(16.w),
border: Border.all(color: Colors.grey.shade300, width: 1.w),
),
child: GarphicWidget(
gradientColors: [
Colors.amber.shade200,
Colors.orange,
],
child: Consumer<PhProvider>(
builder: (context, provider, child) {
final dataState = provider.dataState;
switch (dataState) {
case ResultState.loading:
return const Center(child: CupertinoActivityIndicator());
case ResultState.hasData:
return GarphicWidget(
gradientColors: const [Colors.cyan, Colors.amber],
hour: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].hour ?? 0
: provider.dataFetchedNpk2[index].hour ?? 0,
),
data: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].soilph ?? 0
: provider.dataFetchedNpk2[index].soilph ?? 0,
),
)
],
maxValue: 14,
);
case ResultState.error:
return const GraphicErrorWidget(message: 'Terjadi Kesalahan');
case ResultState.noData:
return const GraphicErrorWidget(message: 'Tidak Ada Data');
case ResultState.initial:
default:
return const SizedBox.shrink();
}
},
),
),
);

View File

@ -0,0 +1,57 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class PhosporusProvider extends ChangeNotifier {
PhosporusProvider() {
getSoilPhosporNpk1Data();
getSoilPhosporNpk2Data();
}
ResultState dataState = ResultState.initial;
List<Npk> dataFetchedNpk1 = [];
List<Npk> dataFetchedNpk2 = [];
Future<void> getSoilPhosporNpk1Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGraphicDataNpk1(metric: 'soilPhosphorus');
if (result.data == null || result.data!.npk1!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk1 = result.data!.npk1 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
Future<void> getSoilPhosporNpk2Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGraphicDataNpk2(metric: 'soilPhosphorus');
if (result.data == null || result.data!.npk2!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk2 = result.data!.npk2 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -1,21 +1,30 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/phosphorus/provider/phosporus_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_error_widget.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
class PhosphorusScreen extends StatelessWidget {
const PhosphorusScreen({super.key, this.phosphorus = 0.0});
const PhosphorusScreen(
{super.key, this.phosphorusNpk1 = 0.0, this.phosphorusNpk2 = 0.0});
final double phosphorus;
double get value => phosphorus;
final double phosphorusNpk1;
final double phosphorusNpk2;
@override
Widget build(BuildContext context) {
return Scaffold(
return ChangeNotifierProvider(
create: (context) => PhosporusProvider(),
child: DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text('Phosphorus', style: AppTheme.labelMedium),
title: Text('Soil Phosphorus', style: AppTheme.labelMedium),
centerTitle: true,
backgroundColor: Colors.white,
scrolledUnderElevation: 0,
@ -23,18 +32,51 @@ class PhosphorusScreen extends StatelessWidget {
Padding(
padding: EdgeInsets.only(right: 16),
child: Icon(
CupertinoIcons.eyedropper,
BootstrapIcons.eyedropper,
color: Colors.blue,
),
)
],
bottom: const TabBar(
tabs: [
Tab(text: 'NPK 1'),
Tab(text: 'NPK 2'),
],
),
body: Center(
),
body: TabBarView(
children: [
buildTabContent(context, phosphorusNpk1, 'NPK 1', true),
buildTabContent(context, phosphorusNpk2, 'NPK 2', false),
],
),
),
),
);
}
SafeArea buildTabContent(
BuildContext context, double value, String label, bool isNpk1) {
return SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(height: 32.h),
Column(
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
buildSoilInfo(context, value),
SizedBox(height: 16.h),
buildInfoRow(context),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
buildGraphicContent(context, isNpk1),
],
),
);
}
Widget buildSoilInfo(BuildContext context, double value) {
return Center(
child: Column(
children: [
Icon(
CupertinoIcons.eyedropper,
@ -44,12 +86,15 @@ class PhosphorusScreen extends StatelessWidget {
Text('$value ppm', style: AppTheme.headline1),
],
),
SizedBox(height: 32.h),
Row(
);
}
Widget buildInfoRow(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Fosfor',
'Soil Phosphorus',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
@ -57,13 +102,14 @@ class PhosphorusScreen extends StatelessWidget {
iconSize: 20.r,
color: Colors.blue,
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle))
],
icon: const Icon(BootstrapIcons.info_circle),
),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
],
);
}
Widget buildGraphicContent(BuildContext context, bool isNpk1) {
return AspectRatio(
aspectRatio: 1.6.h,
child: Container(
decoration: BoxDecoration(
@ -71,15 +117,43 @@ class PhosphorusScreen extends StatelessWidget {
borderRadius: BorderRadius.circular(16.w),
border: Border.all(color: Colors.grey.shade300, width: 1.w),
),
child: GarphicWidget(
gradientColors: [
Colors.blue.shade200,
Colors.blue,
],
child: Consumer<PhosporusProvider>(
builder: (context, provider, child) {
final dataState = provider.dataState;
switch (dataState) {
case ResultState.loading:
return const Center(child: CupertinoActivityIndicator());
case ResultState.hasData:
return GarphicWidget(
gradientColors: const [Colors.blue, Colors.blueAccent],
hour: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].hour ?? 0
: provider.dataFetchedNpk2[index].hour ?? 0,
),
data: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].soilphosphorus ?? 0
: provider.dataFetchedNpk2[index].soilphosphorus ?? 0,
),
)
],
maxValue: 10,
);
case ResultState.error:
return const GraphicErrorWidget(message: 'Terjadi Kesalahan');
case ResultState.noData:
return const GraphicErrorWidget(message: 'Tidak Ada Data');
case ResultState.initial:
default:
return const SizedBox.shrink();
}
},
),
),
);

View File

@ -0,0 +1,57 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class PotassiumProvider extends ChangeNotifier {
PotassiumProvider() {
getSoilPotassiumNpk1Data();
getSoilPotassiumNpk2Data();
}
ResultState dataState = ResultState.initial;
List<Npk> dataFetchedNpk1 = [];
List<Npk> dataFetchedNpk2 = [];
Future<void> getSoilPotassiumNpk1Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGraphicDataNpk1(metric: 'soilPotassium');
if (result.data == null || result.data!.npk1!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk1 = result.data!.npk1 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
Future<void> getSoilPotassiumNpk2Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGraphicDataNpk2(metric: 'soilPotassium');
if (result.data == null || result.data!.npk2!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk2 = result.data!.npk2 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -1,21 +1,30 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/potassium/provider/potassium_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_error_widget.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
class PotassiumScreen extends StatelessWidget {
const PotassiumScreen({super.key, this.potassium = 0.0});
const PotassiumScreen(
{super.key, this.potassiumNpk1 = 0.0, this.potassiumNpk2 = 0.0});
final double potassium;
double get value => potassium;
final double potassiumNpk1;
final double potassiumNpk2;
@override
Widget build(BuildContext context) {
return Scaffold(
return ChangeNotifierProvider(
create: (context) => PotassiumProvider(),
child: DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text('Potassium', style: AppTheme.labelMedium),
title: Text('Soil Potassium', style: AppTheme.labelMedium),
centerTitle: true,
backgroundColor: Colors.white,
scrolledUnderElevation: 0,
@ -23,33 +32,69 @@ class PotassiumScreen extends StatelessWidget {
Padding(
padding: EdgeInsets.only(right: 16),
child: Icon(
CupertinoIcons.eyedropper,
color: Colors.green,
BootstrapIcons.eyedropper,
color: Colors.red,
),
)
],
bottom: const TabBar(
tabs: [
Tab(text: 'NPK 1'),
Tab(text: 'NPK 2'),
],
),
body: Center(
),
body: TabBarView(
children: [
buildTabContent(context, potassiumNpk1, 'NPK 1', true),
buildTabContent(context, potassiumNpk2, 'NPK 2', false),
],
),
),
),
);
}
SafeArea buildTabContent(
BuildContext context, double value, String label, bool isNpk1) {
return SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(height: 32.h),
Column(
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
buildSoilInfo(context, value),
SizedBox(height: 16.h),
buildInfoRow(context),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
buildGraphicContent(context, isNpk1),
],
),
);
}
Widget buildSoilInfo(BuildContext context, double value) {
return Center(
child: Column(
children: [
Icon(
CupertinoIcons.eyedropper,
size: 64.r,
color: Colors.green,
color: Colors.red,
),
Text('$value ppm', style: AppTheme.headline1),
],
),
SizedBox(height: 32.h),
Row(
);
}
Widget buildInfoRow(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Kalium',
'Soil Potassium',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
@ -57,13 +102,14 @@ class PotassiumScreen extends StatelessWidget {
iconSize: 20.r,
color: Colors.blue,
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle))
],
icon: const Icon(BootstrapIcons.info_circle),
),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
],
);
}
Widget buildGraphicContent(BuildContext context, bool isNpk1) {
return AspectRatio(
aspectRatio: 1.6.h,
child: Container(
decoration: BoxDecoration(
@ -71,15 +117,43 @@ class PotassiumScreen extends StatelessWidget {
borderRadius: BorderRadius.circular(16.w),
border: Border.all(color: Colors.grey.shade300, width: 1.w),
),
child: const GarphicWidget(
gradientColors: [
Colors.teal,
Colors.green,
],
child: Consumer<PotassiumProvider>(
builder: (context, provider, child) {
final dataState = provider.dataState;
switch (dataState) {
case ResultState.loading:
return const Center(child: CupertinoActivityIndicator());
case ResultState.hasData:
return GarphicWidget(
gradientColors: const [Colors.red, Colors.orange],
hour: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].hour ?? 0
: provider.dataFetchedNpk2[index].hour ?? 0,
),
data: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].soilpotassium ?? 0
: provider.dataFetchedNpk2[index].soilpotassium ?? 0,
),
)
],
maxValue: 1,
);
case ResultState.error:
return const GraphicErrorWidget(message: 'Terjadi Kesalahan');
case ResultState.noData:
return const GraphicErrorWidget(message: 'Tidak Ada Data');
case ResultState.initial:
default:
return const SizedBox.shrink();
}
},
),
),
);

View File

@ -0,0 +1,57 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class SoilMoistureProvider extends ChangeNotifier {
SoilMoistureProvider() {
getSoilMosNpk1Data();
getSoilMosNpk2Data();
}
ResultState dataState = ResultState.initial;
List<Npk> dataFetchedNpk1 = [];
List<Npk> dataFetchedNpk2 = [];
Future<void> getSoilMosNpk1Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGraphicDataNpk1(metric: 'soilhumidity');
if (result.data == null || result.data!.npk1!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk1 = result.data!.npk1 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
Future<void> getSoilMosNpk2Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGraphicDataNpk2(metric: 'soilhumidity');
if (result.data == null || result.data!.npk2!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk2 = result.data!.npk2 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -1,45 +1,82 @@
import 'package:agrilink_vocpro/core/constant/app_constant.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/widgets/show_info.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/soil_moisture/provider/soil_moisture_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_error_widget.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:gauge_indicator/gauge_indicator.dart';
import 'package:provider/provider.dart';
class SoilMoistureScreen extends StatelessWidget {
const SoilMoistureScreen({super.key, this.moisture = 0});
const SoilMoistureScreen(
{super.key, this.moistureNpk1 = 0, this.moistureNpk2 = 0});
final double moisture;
double get value => moisture;
final double moistureNpk1;
final double moistureNpk2;
@override
Widget build(BuildContext context) {
return Scaffold(
return ChangeNotifierProvider(
create: (context) => SoilMoistureProvider(),
child: DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text('Soil Moisture', style: AppTheme.labelMedium),
centerTitle: true,
backgroundColor: Colors.white,
leading: IconButton(
icon: const Icon(CupertinoIcons.back),
onPressed: () => Navigator.pop(context),
),
scrolledUnderElevation: 0,
actions: const [
Padding(
padding: EdgeInsets.only(right: 16),
child: Icon(
Icons.water_outlined,
color: Colors.blue,
BootstrapIcons.water,
color: Colors.green,
),
)
],
bottom: const TabBar(
tabs: [
Tab(text: 'NPK 1'),
Tab(text: 'NPK 2'),
],
),
body: SafeArea(
),
body: TabBarView(
children: [
buildTabContent(context, moistureNpk1, 'NPK 1', true),
buildTabContent(context, moistureNpk2, 'NPK 2', false),
],
),
),
),
);
}
SafeArea buildTabContent(
BuildContext context, double value, String label, bool isNpk1) {
return SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
buildSoilMoistureInfo(context, value),
SizedBox(height: 16.h),
buildInfoRow(context),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
buildGraphicContent(context, isNpk1),
],
),
);
}
Widget buildSoilMoistureInfo(BuildContext context, double value) {
return SizedBox(
height: 280.h,
child: Stack(
fit: StackFit.expand,
@ -48,9 +85,8 @@ class SoilMoistureScreen extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(BootstrapIcons.water,
size: 32, color: Colors.blue),
Text('60 %', style: AppTheme.headline1),
const Icon(BootstrapIcons.water, size: 32, color: Colors.blue),
Text('$value %', style: AppTheme.headline1),
],
),
),
@ -80,49 +116,74 @@ class SoilMoistureScreen extends StatelessWidget {
),
],
),
),
SizedBox(height: 16.h),
Row(
);
}
Widget buildInfoRow(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'Soil Temperature',
'Soil Moisture',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
IconButton(
iconSize: 20.r,
color: Colors.blue,
onPressed: () {
showInfo(
context,
'Soil Temperature',
AppConstant.soilTempInfo,
);
},
icon: const Icon(BootstrapIcons.info_circle))
],
onPressed: () {},
icon: const Icon(BootstrapIcons.info_circle),
),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
aspectRatio: 1.8.h,
],
);
}
Widget buildGraphicContent(BuildContext context, bool isNpk1) {
return AspectRatio(
aspectRatio: 1.6.h,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16.w),
border: Border.all(color: Colors.grey.shade300, width: 1.w),
),
child: GarphicWidget(
gradientColors: [
Colors.blue.shade200,
Colors.blue,
],
child: Consumer<SoilMoistureProvider>(
builder: (context, provider, child) {
final dataState = provider.dataState;
switch (dataState) {
case ResultState.loading:
return const Center(child: CupertinoActivityIndicator());
case ResultState.hasData:
return GarphicWidget(
gradientColors: const [Colors.cyan, Colors.blue],
hour: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].hour ?? 0
: provider.dataFetchedNpk2[index].hour ?? 0,
),
data: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].soilhumidity ?? 0
: provider.dataFetchedNpk2[index].soilhumidity ?? 0,
),
)
],
maxValue: 1,
);
case ResultState.error:
return const GraphicErrorWidget(message: 'Terjadi Kesalahan');
case ResultState.noData:
return const GraphicErrorWidget(message: 'Tidak Ada Data');
case ResultState.initial:
default:
return const SizedBox.shrink();
}
},
),
),
);

View File

@ -0,0 +1,57 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class SoilTempProvider extends ChangeNotifier {
SoilTempProvider() {
getSoilTempNpk1Data();
getSoilTempNpk2Data();
}
ResultState dataState = ResultState.initial;
List<Npk> dataFetchedNpk1 = [];
List<Npk> dataFetchedNpk2 = [];
Future<void> getSoilTempNpk1Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGraphicDataNpk1(metric: 'soilTemperature');
if (result.data == null || result.data!.npk1!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk1 = result.data!.npk1 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
Future<void> getSoilTempNpk2Data() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGraphicDataNpk2(metric: 'soilTemperature');
if (result.data == null || result.data!.npk2!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetchedNpk2 = result.data!.npk2 ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Soil Temp Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -1,21 +1,34 @@
import 'package:agrilink_vocpro/features/home/widgets/graphic_error_widget.dart';
import 'package:flutter/material.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:provider/provider.dart';
import 'package:agrilink_vocpro/core/constant/app_constant.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/core/widgets/show_info.dart';
import 'package:agrilink_vocpro/features/home/pages/soil_temperature/provider/soil_temp_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:gauge_indicator/gauge_indicator.dart';
class SoilTemperatureScreen extends StatelessWidget {
const SoilTemperatureScreen({super.key, this.temperature = 0});
const SoilTemperatureScreen({
super.key,
this.npk1Temperature = 0,
this.npk2Temperature = 0,
});
final double temperature;
double get value => temperature;
final double npk1Temperature;
final double npk2Temperature;
@override
Widget build(BuildContext context) {
return Scaffold(
return ChangeNotifierProvider(
create: (context) => SoilTempProvider(),
child: DefaultTabController(
length: 2,
child: Scaffold(
appBar: AppBar(
title: Text('Soil Temperature', style: AppTheme.labelMedium),
centerTitle: true,
@ -30,15 +43,48 @@ class SoilTemperatureScreen extends StatelessWidget {
),
)
],
bottom: const TabBar(
tabs: [
Tab(text: 'NPK 1'),
Tab(text: 'NPK 2'),
],
),
body: SafeArea(
),
body: TabBarView(
children: [
buildTabContent(context, npk1Temperature, 'NPK 1', true),
buildTabContent(context, npk2Temperature, 'NPK 2', false),
],
),
),
),
);
}
// Generalized method for tab content to avoid duplication
SafeArea buildTabContent(
BuildContext context, double value, String label, bool isNpk1) {
return SafeArea(
child: ListView(
padding: EdgeInsets.all(16.w),
children: [
SizedBox(
height: MediaQuery.of(context).size.height * 0.05,
SizedBox(height: MediaQuery.of(context).size.height * 0.05),
buildTemperatureInfo(context, value),
SizedBox(height: 16.h),
buildInfoRow(context),
SizedBox(height: 16.h),
buildTemperatureRange(),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
buildGraphicContent(context, isNpk1),
],
),
SizedBox(
);
}
Widget buildTemperatureInfo(BuildContext context, double value) {
return SizedBox(
height: 240.h,
child: Stack(
fit: StackFit.expand,
@ -47,13 +93,10 @@ class SoilTemperatureScreen extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
SizedBox(
height: 80.h,
),
const Icon(BootstrapIcons.water,
size: 32, color: Colors.green),
SizedBox(height: 80.h),
const Icon(BootstrapIcons.water, size: 32, color: Colors.green),
Text(
'${value.toStringAsFixed(0)}°C', // Animated percentage text
'$value°C', // Display temperature
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
@ -85,9 +128,46 @@ class SoilTemperatureScreen extends StatelessWidget {
),
],
),
);
}
// Build the row with 'Low', 'Ideal', 'High' temperature containers
Widget buildTemperatureRange() {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildInfoContainer('Low', '<20°C', Colors.blue),
_buildInfoContainer('Ideal', '20-30°C', Colors.green),
_buildInfoContainer('High', '>30°C', Colors.orange),
],
);
}
// Reusable container builder
Widget _buildInfoContainer(String label, String tempRange, Color color) {
return Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(color: color, width: 2),
),
SizedBox(height: 16.h),
Row(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(label, style: AppTheme.labelMedium.copyWith(color: color)),
SizedBox(height: 8.h),
Text(tempRange,
style: AppTheme.labelMedium, textAlign: TextAlign.center),
],
),
);
}
// Information row with IconButton
Widget buildInfoRow(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
@ -99,118 +179,17 @@ class SoilTemperatureScreen extends StatelessWidget {
iconSize: 20.r,
color: Colors.blue,
onPressed: () {
showInfo(
context,
'Soil Temperature',
AppConstant.soilTempInfo,
);
showInfo(context, 'Soil Temperature', AppConstant.soilTempInfo);
},
icon: const Icon(BootstrapIcons.info_circle))
],
),
SizedBox(height: 16.h),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(16),
color: Colors.blue.withOpacity(0.1),
border: Border.all(
color: Colors.blue,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Low',
style: AppTheme.labelMedium
.copyWith(color: Colors.blue)),
// SizedBox(height: 8.h),
// const Icon(
// BootstrapIcons.thermometer_low,
// color: Colors.blue,
// ),
SizedBox(height: 8.h),
Text(
'<20°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
icon: const Icon(BootstrapIcons.info_circle),
),
],
),
),
Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
color: Colors.green.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.green,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('Ideal',
style: AppTheme.labelMedium
.copyWith(color: Colors.green)),
// SizedBox(height: 8.h),
// const Icon(
// BootstrapIcons.thermometer_half,
// color: Colors.green,
// ),
SizedBox(height: 8.h),
Text(
'20-30°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
],
),
),
Container(
height: 100.h,
width: 100.w,
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: Colors.orange.shade800,
width: 2,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('high',
style: AppTheme.labelMedium
.copyWith(color: Colors.orange)),
// SizedBox(height: 8.h),
// const Icon(
// BootstrapIcons.thermometer_high,
// color: Colors.orange,
// ),
SizedBox(height: 8.h),
Text(
'>30°C',
style: AppTheme.labelMedium,
textAlign: TextAlign.center,
),
],
),
),
],
),
SizedBox(height: 16.h),
const Text('Grafik'),
SizedBox(height: 16.h),
AspectRatio(
);
}
// Generalized method to build the graphic content based on NPK type
Widget buildGraphicContent(BuildContext context, bool isNpk1) {
return AspectRatio(
aspectRatio: 1.6.h,
child: Container(
decoration: BoxDecoration(
@ -218,15 +197,43 @@ class SoilTemperatureScreen extends StatelessWidget {
borderRadius: BorderRadius.circular(16.w),
border: Border.all(color: Colors.grey.shade300, width: 1.w),
),
child: const GarphicWidget(
gradientColors: [
Colors.cyan,
Colors.amber,
],
child: Consumer<SoilTempProvider>(
builder: (context, provider, child) {
final dataState = provider.dataState;
switch (dataState) {
case ResultState.loading:
return const Center(child: CupertinoActivityIndicator());
case ResultState.hasData:
return GarphicWidget(
gradientColors: const [Colors.cyan, Colors.amber],
hour: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].hour ?? 0
: provider.dataFetchedNpk2[index].hour ?? 0,
),
data: List.generate(
isNpk1
? provider.dataFetchedNpk1.length
: provider.dataFetchedNpk2.length,
(index) => isNpk1
? provider.dataFetchedNpk1[index].soiltemperature ?? 0
: provider.dataFetchedNpk2[index].soiltemperature ?? 0,
),
)
],
maxValue: 70,
);
case ResultState.error:
return const GraphicErrorWidget(message: 'Terjadi Kesalahan');
case ResultState.noData:
return const GraphicErrorWidget(message: 'Tidak Ada Data');
case ResultState.initial:
default:
return const SizedBox.shrink();
}
},
),
),
);

View File

@ -0,0 +1,34 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/data/model/latest_data_response.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class TempProvider extends ChangeNotifier {
TempProvider() {
getTempData();
}
ResultState dataState = ResultState.initial;
List<Dht> dataFetched = [];
Future<void> getTempData() async {
dataState = ResultState.loading;
notifyListeners();
try {
final result =
await AppService().getGrafikDataDht(metric: 'viciTemperature');
if (result.data == null || result.data!.dht!.isEmpty) {
dataState = ResultState.noData;
} else {
dataFetched = result.data!.dht ?? [];
dataState = ResultState.hasData;
}
} catch (e) {
if (kDebugMode) {
print('Get Grafik Temperature Error: $e');
}
dataState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -1,9 +1,13 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/temperature/provider/temp_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/graphic_widget.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:gauge_indicator/gauge_indicator.dart';
import 'package:provider/provider.dart';
class TemperatureScreen extends StatelessWidget {
const TemperatureScreen({super.key, this.temperature = 0});
@ -13,7 +17,9 @@ class TemperatureScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return Scaffold(
return ChangeNotifierProvider(
create: (context) => TempProvider(),
child: Scaffold(
appBar: AppBar(
title: Text('Temperature', style: AppTheme.labelMedium),
centerTitle: true,
@ -122,11 +128,6 @@ class TemperatureScreen extends StatelessWidget {
Text('Low',
style: AppTheme.labelMedium
.copyWith(color: Colors.blue)),
// SizedBox(height: 8.h),
// const Icon(
// BootstrapIcons.thermometer_low,
// color: Colors.blue,
// ),
SizedBox(height: 8.h),
Text(
'<20°C',
@ -153,11 +154,6 @@ class TemperatureScreen extends StatelessWidget {
Text('Ideal',
style: AppTheme.labelMedium
.copyWith(color: Colors.green)),
// SizedBox(height: 8.h),
// const Icon(
// BootstrapIcons.thermometer_half,
// color: Colors.green,
// ),
SizedBox(height: 8.h),
Text(
'20-30°C',
@ -184,11 +180,6 @@ class TemperatureScreen extends StatelessWidget {
Text('high',
style: AppTheme.labelMedium
.copyWith(color: Colors.orange)),
// SizedBox(height: 8.h),
// const Icon(
// BootstrapIcons.thermometer_high,
// color: Colors.orange,
// ),
SizedBox(height: 8.h),
Text(
'>30°C',
@ -211,17 +202,80 @@ class TemperatureScreen extends StatelessWidget {
borderRadius: BorderRadius.circular(16.w),
border: Border.all(color: Colors.grey.shade300, width: 1.w),
),
child: const GarphicWidget(
gradientColors: [
child: Consumer<TempProvider>(
builder: (context, provider, child) {
switch (provider.dataState) {
case ResultState.loading:
return const Center(
child: CupertinoActivityIndicator(),
);
case ResultState.hasData:
return GarphicWidget(
gradientColors: const [
Colors.cyan,
Colors.amber,
],
hour: List.generate(
provider.dataFetched.length,
(index) =>
provider.dataFetched[index].hour ?? 0),
data: List.generate(
provider.dataFetched.length,
(index) =>
provider.dataFetched[index].vicitemperature
?.toDouble() ??
0),
);
case ResultState.error:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.exclamation_circle,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Terjadi Kesalahan',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.noData:
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(
BootstrapIcons.database_fill_x,
color: Colors.grey.shade400,
),
SizedBox(height: 8.h),
Text(
'Tidak Ada Data',
style: AppTheme.labelSmall,
),
],
),
);
case ResultState.initial:
return const SizedBox.shrink();
default:
return const Center(
child: Text('Default Error'),
);
}
},
),
),
)
],
),
),
),
);
}
}

View File

@ -1,25 +1,103 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:flutter/material.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
class HomeProvider extends ChangeNotifier {
final DateTime currentDate = DateTime.now();
num _dhtHumidity = 0;
num get dhtHumidity => _dhtHumidity;
num _dhtTemperature = 0;
num get dhtTemperature => _dhtTemperature;
num _dhtLuminosity = 0;
num get dhtLuminosity => _dhtLuminosity;
num _npk1SoilMoisture = 0;
num get npk1SoilMoisture => _npk1SoilMoisture;
num _npk1Temperature = 0;
num get npk1Temperature => _npk1Temperature;
num _npk1SoilPh = 0;
num get npk1SoilPh => _npk1SoilPh;
num _npk1SoilEc = 0;
num get npk1SoilEc => _npk1SoilEc;
num _npk1SoilNitrogen = 0;
num get npk1SoilNitrogen => _npk1SoilNitrogen;
num _npk1SoilPhosphorus = 0;
num get npk1SoilPhosphorus => _npk1SoilPhosphorus;
num _npk1SoilPotassium = 0;
num get npk1SoilPotassium => _npk1SoilPotassium;
num _npk2SoilMoisture = 0;
num get npk2SoilMoisture => _npk2SoilMoisture;
num _npk2Temperature = 0;
num get npk2Temperature => _npk2Temperature;
num _npk2SoilPh = 0;
num get npk2SoilPh => _npk2SoilPh;
num _npk2SoilEc = 0;
num get npk2SoilEc => _npk2SoilEc;
num _npk2SoilNitrogen = 0;
num get npk2SoilNitrogen => _npk2SoilNitrogen;
num _npk2SoilPhosphorus = 0;
num get npk2SoilPhosphorus => _npk2SoilPhosphorus;
num _npk2SoilPotassium = 0;
num get npk2SoilPotassium => _npk2SoilPotassium;
HomeProvider() {
getData();
getLatestData();
}
ResultState dataState = ResultState.initial;
Future<void> getData() async {
Future<void> getLatestData() async {
dataState = ResultState.loading;
notifyListeners();
try {
print('Fetching data...');
await Future.delayed(const Duration(seconds: 3));
print('Data fetched');
final result = await AppService().getLatestData();
if (result.data == null) {
dataState = ResultState.noData;
notifyListeners();
} else {
final data = result.data!;
_dhtHumidity = data.dht?[0].vicihumidity ?? 0;
_dhtTemperature = data.dht?[0].vicitemperature ?? 0;
_dhtLuminosity = data.dht?[0].viciluminosity ?? 0;
_npk1SoilMoisture = data.npk1?[0].soilhumidity ?? 0;
_npk1Temperature = data.npk1?[0].soiltemperature ?? 0;
_npk1SoilPh = data.npk1?[0].soilph ?? 0;
_npk1SoilEc = data.npk1?[0].soilconductivity ?? 0;
_npk1SoilNitrogen = data.npk1?[0].soilnitrogen ?? 0;
_npk1SoilPhosphorus = data.npk1?[0].soilphosphorus ?? 0;
_npk1SoilPotassium = data.npk1?[0].soilpotassium ?? 0;
_npk2SoilMoisture = data.npk2?[0].soilhumidity ?? 0;
_npk2Temperature = data.npk2?[0].soiltemperature ?? 0;
_npk2SoilPh = data.npk2?[0].soilph ?? 0;
_npk2SoilEc = data.npk2?[0].soilconductivity ?? 0;
_npk2SoilNitrogen = data.npk2?[0].soilnitrogen ?? 0;
_npk2SoilPhosphorus = data.npk2?[0].soilphosphorus ?? 0;
_npk2SoilPotassium = data.npk2?[0].soilpotassium ?? 0;
dataState = ResultState.hasData;
notifyListeners();
}
} catch (e) {
if (kDebugMode) {
print('Get Latest Error: $e');
}
dataState = ResultState.error;
notifyListeners();
}
@ -31,57 +109,3 @@ class HomeProvider extends ChangeNotifier {
super.dispose();
}
}
// List<CensorDataRule> humidtyRules = [
// CensorDataRule(
// minPercentage: 0,
// maxPercentage: 30,
// censorText: 'Very Low',
// description:
// 'Udara sangat kering. Tanaman bisa mengalami stress akibat kekurangan air.',
// action:
// 'Aktifkan sistem penyiraman atau humidifier untuk menaikkan kelembaban. Periksa juga apakah ada kebocoran pada sistem irigasi yang mengakibatkan kelembaban terlalu rendah.',
// color: Colors.red,
// ),
// CensorDataRule(
// minPercentage: 31,
// maxPercentage: 50,
// censorText: 'Low',
// description:
// 'Kelembaban masih cukup rendah. Beberapa jenis tanaman mungkin sudah mulai terpengaruh.',
// action:
// 'Pertimbangkan untuk menambah irigasi atau memperpanjang durasi penyiraman. Pantau tanaman secara berkala.',
// color: Colors.orange,
// ),
// CensorDataRule(
// minPercentage: 51,
// maxPercentage: 70,
// censorText: 'Normal',
// description:
// 'Ini adalah kelembaban yang ideal untuk sebagian besar tanaman dalam greenhouse.',
// action:
// 'Pertahankan kondisi ini. Tidak ada tindakan yang diperlukan kecuali jika ada perubahan mendadak.',
// color: Colors.green,
// ),
// CensorDataRule(
// minPercentage: 71,
// maxPercentage: 85,
// censorText: 'High',
// description:
// 'Udara mulai terlalu lembap. Kelembaban tinggi dapat meningkatkan risiko penyakit jamur atau bakteri.',
// action:
// 'Aktifkan ventilasi atau kipas untuk mengurangi kelembaban. Pastikan aliran udara di greenhouse cukup baik.',
// color: Colors.lime,
// ),
// CensorDataRule(
// minPercentage: 86,
// maxPercentage: 100,
// censorText: 'Very High',
// description:
// 'Udara sangat lembap, yang bisa berisiko menyebabkan jamur, lumut, dan penyakit tanaman.',
// action:
// 'Segera aktifkan sistem ventilasi maksimal, mungkin juga gunakan dehumidifier jika diperlukan. Kurangi frekuensi penyiraman atau periksa sistem irigasi agar tidak berlebihan.',
// color: Colors.brown,
// ),
// ];

View File

@ -0,0 +1 @@

View File

@ -5,6 +5,7 @@ import 'package:agrilink_vocpro/features/home/provider/home_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/list_data_from_censor_npk1.dart';
import 'package:agrilink_vocpro/features/home/widgets/list_data_from_censor_npk2.dart';
import 'package:agrilink_vocpro/features/home/widgets/list_data_from_censor_dht.dart';
import 'package:agrilink_vocpro/features/setting/provider/setting_provider.dart';
import 'package:animated_segmented_tab_control/animated_segmented_tab_control.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
@ -29,7 +30,9 @@ class _HomeScreenState extends State<HomeScreen> {
scrolledUnderElevation: 0,
flexibleSpace: Padding(
padding: EdgeInsets.symmetric(horizontal: 12.w),
child: Column(
child:
Consumer<SettingProvider>(builder: (context, settingP, child) {
return Column(
children: [
Row(
children: [
@ -52,7 +55,7 @@ class _HomeScreenState extends State<HomeScreen> {
style: AppTheme.labelSmall,
),
Text(
'Fikril Mahesaputra',
settingP.userFullName,
style: AppTheme.labelMedium,
),
],
@ -60,7 +63,7 @@ class _HomeScreenState extends State<HomeScreen> {
const Spacer(),
IconButton(
onPressed: () {
context.read<HomeProvider>().getData();
context.read<HomeProvider>().getLatestData();
},
icon: const Icon(
Icons.refresh_rounded,
@ -95,7 +98,7 @@ class _HomeScreenState extends State<HomeScreen> {
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Fikril's Greenhouse",
Text("${settingP.userFullName}'s",
style: AppTheme.labelMedium
.copyWith(color: Colors.white)),
Container(
@ -118,7 +121,8 @@ class _HomeScreenState extends State<HomeScreen> {
),
),
],
),
);
}),
),
),
body: DefaultTabController(

View File

@ -0,0 +1,24 @@
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class GraphicErrorWidget extends StatelessWidget {
const GraphicErrorWidget({super.key, required this.message});
final String message;
@override
Widget build(BuildContext context) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(BootstrapIcons.exclamation_circle, color: Colors.grey.shade400),
SizedBox(height: 8.h),
Text(message, style: AppTheme.labelSmall),
],
),
);
}
}

View File

@ -4,9 +4,17 @@ import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class GarphicWidget extends StatelessWidget {
const GarphicWidget({super.key, required this.gradientColors});
const GarphicWidget(
{super.key,
required this.gradientColors,
this.data,
this.hour,
this.maxValue = 100});
final List<Color> gradientColors;
final List<num>? hour;
final List<num>? data;
final double maxValue;
@override
Widget build(BuildContext context) {
@ -80,18 +88,16 @@ class GarphicWidget extends StatelessWidget {
minX: 0,
maxX: 24,
minY: 0,
maxY: 100,
maxY: data == null ? 0 : maxValue,
lineBarsData: [
LineChartBarData(
spots: const [
FlSpot(0, 38),
FlSpot(1, 42),
FlSpot(2, 50),
FlSpot(3, 53),
FlSpot(4, 58),
FlSpot(5, 64),
FlSpot(7, 49),
],
spots: data == null && hour == null
? [FlSpot(0, 0)]
: List.generate(
hour!.length,
(index) =>
FlSpot(hour![index].toDouble(), data![index].toDouble()),
),
isCurved: true,
gradient: LinearGradient(
colors: gradientColors,

View File

@ -50,7 +50,7 @@ class ListDataFromCensorDht extends StatelessWidget {
DataDisplayerWidget(
title: 'Humidity',
subtitle: 'kelembaban udara',
value: '60',
value: provider.dhtHumidity.toString(),
unit: '%',
icon: BootstrapIcons.droplet_half,
textColor: Colors.white,
@ -58,29 +58,32 @@ class ListDataFromCensorDht extends StatelessWidget {
iconColor: Colors.white,
censorIdentifier: 'NPK 1',
onTap: () async {
await context.push('${AppRoute.humidity}/60');
await context
.push('${AppRoute.humidity}/${provider.dhtHumidity}');
},
),
DataDisplayerWidget(
title: 'Temperature',
subtitle: 'suhu greenhouse',
value: '43',
value: provider.dhtTemperature.toString(),
unit: '°C',
icon: BootstrapIcons.thermometer_half,
color: Colors.white,
onTap: () async {
await context.push('${AppRoute.temperature}/43');
await context.push(
'${AppRoute.temperature}/${provider.dhtTemperature}');
},
),
DataDisplayerWidget(
title: 'Light',
subtitle: 'intensitas cahaya',
value: '320.5',
value: provider.dhtLuminosity.toString(),
unit: 'lux',
icon: BootstrapIcons.sun,
color: Colors.white,
onTap: () async {
await context.push('${AppRoute.light}/320.5');
await context
.push('${AppRoute.light}/${provider.dhtLuminosity}');
},
),
],

View File

@ -49,7 +49,7 @@ class ListDataFromCensorNpk1 extends StatelessWidget {
DataDisplayerWidget(
title: 'Temperature',
subtitle: 'Suhu tanah',
value: '28',
value: provider.npk1Temperature.toString(),
unit: '°C',
icon: BootstrapIcons.thermometer_half,
textColor: Colors.white,
@ -57,76 +57,83 @@ class ListDataFromCensorNpk1 extends StatelessWidget {
iconColor: Colors.white,
censorIdentifier: censorIdentifier,
onTap: () async {
await context.push('${AppRoute.soilTemperature}/28');
await context.push(
'${AppRoute.soilTemperature}/${provider.npk1Temperature}/${provider.npk2Temperature}');
},
),
DataDisplayerWidget(
title: 'Soil Moisture',
subtitle: 'kelembaban tanah',
value: '40',
value: provider.npk1SoilMoisture.toString(),
unit: '%',
icon: Icons.water_outlined,
color: Colors.white,
censorIdentifier: censorIdentifier,
onTap: () async {
await context.push('${AppRoute.soilMoisture}/40');
await context.push(
'${AppRoute.soilMoisture}/${provider.npk1SoilMoisture}/${provider.npk2SoilMoisture}');
},
),
DataDisplayerWidget(
title: 'Acid Level (PH)',
subtitle: 'tingkat keasaman',
value: '6.5',
value: provider.npk1SoilPh.toString(),
unit: 'pH',
icon: BootstrapIcons.pie_chart,
color: Colors.white,
censorIdentifier: censorIdentifier,
onTap: () {
context.push('${AppRoute.ph}/6.5');
onTap: () async {
context.push(
'${AppRoute.ph}/${provider.npk1SoilPh}/${provider.npk2SoilPh}');
},
),
DataDisplayerWidget(
title: 'Conductivity',
subtitle: 'Daya Arus Listrik',
value: '234',
value: provider.npk1SoilEc.toString(),
unit: 'µS/cm',
icon: Icons.electric_bolt,
color: Colors.white,
censorIdentifier: censorIdentifier,
onTap: () async {
await context.push('${AppRoute.conductivity}/234');
await context.push(
'${AppRoute.conductivity}/${provider.npk1SoilEc}/${provider.npk2SoilEc}');
},
),
DataDisplayerWidget(
title: 'Nitrogen',
subtitle: 'Kadar Nitrogen',
value: '30',
value: provider.npk1SoilNitrogen.toString(),
unit: 'ppm',
icon: CupertinoIcons.eyedropper,
color: Colors.white,
onTap: () async {
await context.push('${AppRoute.nitrogen}/30');
await context.push(
'${AppRoute.nitrogen}/${provider.npk2SoilNitrogen}/${provider.npk1SoilNitrogen}');
},
),
DataDisplayerWidget(
title: 'Potassium',
subtitle: 'Kadar kalium',
value: '20',
value: provider.npk1SoilPotassium.toString(),
unit: 'ppm',
icon: CupertinoIcons.eyedropper,
color: Colors.white,
onTap: () async {
await context.push('${AppRoute.potassium}/20');
await context.push(
'${AppRoute.potassium}/${provider.npk1SoilPotassium}/${provider.npk2SoilPotassium}');
},
),
DataDisplayerWidget(
title: 'Phosphorus',
subtitle: 'Kadar Fosfor',
value: '54',
value: provider.npk1SoilPhosphorus.toString(),
unit: 'ppm',
icon: CupertinoIcons.eyedropper,
color: Colors.white,
onTap: () async {
await context.push('${AppRoute.phosphorus}/54');
await context.push(
'${AppRoute.phosphorus}/${provider.npk1SoilPhosphorus}/${provider.npk2SoilPhosphorus}');
},
),
],

View File

@ -1,8 +1,6 @@
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/route/app_route.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/home/pages/soil_moisture/view/soil_moisture_screen.dart';
import 'package:agrilink_vocpro/features/home/pages/temperature/view/temperature_screen.dart';
import 'package:agrilink_vocpro/features/home/provider/home_provider.dart';
import 'package:agrilink_vocpro/features/home/widgets/censor_item_loading_widgets.dart';
import 'package:agrilink_vocpro/features/home/widgets/data_display_widget.dart';
@ -51,7 +49,7 @@ class ListDataFromCensorNpk2 extends StatelessWidget {
DataDisplayerWidget(
title: 'Temperature',
subtitle: 'Suhu tanah',
value: '28',
value: provider.npk2Temperature.toString(),
unit: '°C',
icon: BootstrapIcons.thermometer_half,
textColor: Colors.white,
@ -59,77 +57,84 @@ class ListDataFromCensorNpk2 extends StatelessWidget {
iconColor: Colors.white,
censorIdentifier: censorIdentifier,
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const TemperatureScreen()));
await context.push(
'${AppRoute.soilTemperature}/${provider.npk1Temperature}/${provider.npk2Temperature}');
},
),
DataDisplayerWidget(
title: 'Soil Moisture',
subtitle: 'kelembaban tanah',
value: '40',
value: provider.npk2SoilMoisture.toString(),
unit: '%',
icon: Icons.water_outlined,
color: Colors.white,
censorIdentifier: censorIdentifier,
onTap: () async {
await Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SoilMoistureScreen(),
),
);
await context.push(
'${AppRoute.soilMoisture}/${provider.npk1SoilMoisture}/${provider.npk2SoilMoisture}');
},
),
DataDisplayerWidget(
title: 'Acid Level (PH)',
subtitle: 'tingkat keasaman',
value: '6.5',
value: provider.npk2SoilPh.toString(),
unit: 'pH',
icon: BootstrapIcons.pie_chart,
color: Colors.white,
censorIdentifier: censorIdentifier,
onTap: () {},
onTap: () async {
context.push(
'${AppRoute.ph}/${provider.npk1SoilPh}/${provider.npk2SoilPh}');
},
),
DataDisplayerWidget(
title: 'Conductivity',
subtitle: 'Daya Arus Listrik',
value: '234',
value: provider.npk2SoilEc.toString(),
unit: 'µS/cm',
icon: Icons.electric_bolt,
color: Colors.white,
censorIdentifier: censorIdentifier,
onTap: () async {
context.push(AppRoute.humidity, extra: '60');
await context.push(
'${AppRoute.conductivity}/${provider.npk1SoilEc}/${provider.npk2SoilEc}');
},
),
DataDisplayerWidget(
title: 'Nitrogen',
subtitle: 'Kadar Nitrogen',
value: '30',
value: provider.npk2SoilNitrogen.toString(),
unit: 'ppm',
icon: CupertinoIcons.eyedropper,
color: Colors.white,
onTap: () {},
onTap: () async {
await context.push(
'${AppRoute.nitrogen}/${provider.npk2SoilNitrogen}/${provider.npk1SoilNitrogen}');
},
),
DataDisplayerWidget(
title: 'Potassium',
subtitle: 'Kadar kalium',
value: '20',
value: provider.npk2SoilPotassium.toString(),
unit: 'ppm',
icon: CupertinoIcons.eyedropper,
color: Colors.white,
onTap: () {},
onTap: () async {
await context.push(
'${AppRoute.potassium}/${provider.npk1SoilPotassium}/${provider.npk2SoilPotassium}');
},
),
DataDisplayerWidget(
title: 'Phosphorus',
subtitle: 'Kadar Fosfor',
value: '54',
value: provider.npk2SoilPhosphorus.toString(),
unit: 'ppm',
icon: CupertinoIcons.eyedropper,
color: Colors.white,
onTap: () {},
onTap: () async {
await context.push(
'${AppRoute.phosphorus}/${provider.npk1SoilPhosphorus}/${provider.npk2SoilPhosphorus}');
},
),
],
);

View File

@ -1,5 +1,7 @@
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
class PlantsScreen extends StatelessWidget {
const PlantsScreen({super.key});
@ -9,7 +11,19 @@ class PlantsScreen extends StatelessWidget {
return Scaffold(
body: SafeArea(
child: Center(
child: Text('Coming Soon', style: AppTheme.labelMedium),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.eco_rounded, size: 100.r, color: AppColor.secondary),
Text('Coming Soon', style: AppTheme.titleLarge),
SizedBox(height: 8.h),
Text(
'This featureis under development',
textAlign: TextAlign.center,
style: AppTheme.labelSmall,
),
],
),
)),
);
}

View File

@ -0,0 +1,40 @@
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/domain/service/app_service.dart';
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SettingProvider extends ChangeNotifier {
SettingProvider() {
_init();
}
String userFullName = '';
String userEmail = '';
void _init() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
userFullName = prefs.getString('fullName') ?? 'unknown';
userEmail = prefs.getString('email') ?? 'unknown';
notifyListeners();
}
ResultState logoutState = ResultState.initial;
Future<void> logout() async {
logoutState = ResultState.loading;
notifyListeners();
try {
final result = await AppService().logout();
if (result.data == null) {
logoutState = ResultState.hasData;
} else {
logoutState = ResultState.error;
}
} catch (e) {
if (kDebugMode) {
print('Error logout: $e');
}
logoutState = ResultState.error;
}
notifyListeners();
}
}

View File

@ -1,10 +1,13 @@
import 'package:agrilink_vocpro/core/constant/app_color.dart';
import 'package:agrilink_vocpro/core/constant/app_theme.dart';
import 'package:agrilink_vocpro/core/route/app_route.dart';
import 'package:agrilink_vocpro/core/state/result_state.dart';
import 'package:agrilink_vocpro/features/setting/provider/setting_provider.dart';
import 'package:bootstrap_icons/bootstrap_icons.dart';
import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
class SettingScreen extends StatelessWidget {
const SettingScreen({super.key});
@ -18,7 +21,8 @@ class SettingScreen extends StatelessWidget {
backgroundColor: Colors.white,
scrolledUnderElevation: 0,
),
body: SafeArea(
body: Consumer<SettingProvider>(builder: (context, provider, child) {
return SafeArea(
child: ListView(
padding: EdgeInsets.all(16.r),
children: [
@ -33,8 +37,8 @@ class SettingScreen extends StatelessWidget {
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('User Name', style: AppTheme.labelMedium),
Text('useremail@gmail.com', style: AppTheme.labelSmall)
Text(provider.userFullName, style: AppTheme.labelMedium),
Text(provider.userEmail, style: AppTheme.labelSmall)
],
)
],
@ -49,8 +53,8 @@ class SettingScreen extends StatelessWidget {
ListTile(
tileColor: Colors.white,
title: Text('Account',
style:
AppTheme.labelSmall.copyWith(color: Colors.black87)),
style: AppTheme.labelSmall
.copyWith(color: Colors.black87)),
leading: const Icon(BootstrapIcons.person),
trailing: Icon(
Icons.arrow_forward_ios,
@ -61,8 +65,8 @@ class SettingScreen extends StatelessWidget {
ListTile(
tileColor: Colors.white,
title: Text('Kebijakan & privasi',
style:
AppTheme.labelSmall.copyWith(color: Colors.black87)),
style: AppTheme.labelSmall
.copyWith(color: Colors.black87)),
leading: const Icon(BootstrapIcons.shield_check),
trailing: Icon(
Icons.arrow_forward_ios,
@ -73,8 +77,8 @@ class SettingScreen extends StatelessWidget {
ListTile(
tileColor: Colors.white,
title: Text('Syarat & ketentuan',
style:
AppTheme.labelSmall.copyWith(color: Colors.black87)),
style: AppTheme.labelSmall
.copyWith(color: Colors.black87)),
leading: const Icon(BootstrapIcons.file_text),
trailing: Icon(
Icons.arrow_forward_ios,
@ -109,19 +113,27 @@ class SettingScreen extends StatelessWidget {
),
TextButton(
child: Text('Ya'),
onPressed: () {
onPressed: () async {
await provider.logout();
if (context.mounted) {
if (provider.logoutState ==
ResultState.hasData) {
context.go(AppRoute.root);
}
}
},
),
],
),
);
},
),
],
),
),
],
));
}),
],
),
),
],
)),
);
}
}

View File

@ -1,8 +1,12 @@
import 'dart:async';
import 'package:agrilink_vocpro/core/route/app_route.dart';
import 'package:firebase_messaging/firebase_messaging.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:jwt_decoder/jwt_decoder.dart';
import 'package:shared_preferences/shared_preferences.dart';
class SplashScreen extends StatefulWidget {
const SplashScreen({super.key});
@ -17,37 +21,41 @@ class _SplashScreenState extends State<SplashScreen> {
@override
void initState() {
super.initState();
FirebaseMessaging.instance.subscribeToTopic('smartfarm');
_initialize();
}
Future<void> _initialize() async {
// final authProvider = Provider.of<AuthProvider>(context, listen: false);
// bool isLoggedIn = await _checkLoginStatus(authProvider);
bool isLoggedIn = await _checkLoginStatus();
_navigateAfterSplash(isLoggedIn);
}
// Future<bool> _checkLoginStatus(AuthProvider authProvider) async {
// SharedPreferences prefs = await SharedPreferences.getInstance();
Future<bool> _checkLoginStatus() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
// if (prefs.getKeys().isEmpty) return false;
if (prefs.getKeys().isEmpty) return false;
// if (prefs.getBool('isLoggedIn') == true) {
// String? token = prefs.getString('token');
// String? refreshToken = prefs.getString('refreshToken');
if (prefs.getBool('isLoggedIn') == true) {
String? token = prefs.getString('jwtToken');
// if (token != null && !JwtDecoder.isExpired(token)) {
// return true;
// } else if (refreshToken != null && !JwtDecoder.isExpired(refreshToken)) {
// final result = await authProvider.refreshToken();
// return result == ResultState.hasData;
// } else {
// prefs.remove('token');
// return false;
// }
// }
if (token != null && !JwtDecoder.isExpired(token)) {
if (kDebugMode) {
print('Token : ${prefs.getString('token')}');
}
return true;
} else {
prefs.remove('token');
prefs.remove('jwtToken');
prefs.remove('username');
prefs.remove('email');
prefs.remove('fullName');
prefs.remove('isLoggedIn');
return false;
}
}
// return false;
// }
return false;
}
void _navigateAfterSplash(bool isLoggedIn) {
Timer(const Duration(seconds: 2), () {

View File

@ -0,0 +1,68 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for web - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for macos - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.windows:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for windows - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyCB8i2dE3Oc0kTNtVPw_qSz-T8gPYNjxFk',
appId: '1:445047869982:android:d40dc2fd624f86a315540f',
messagingSenderId: '445047869982',
projectId: 'agrilink-vocpro-b37f9',
storageBucket: 'agrilink-vocpro-b37f9.appspot.com',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyDZO8tEZFcQrJMwJG_viJTbITWNVqxJo8E',
appId: '1:445047869982:ios:9350ac31c363c40415540f',
messagingSenderId: '445047869982',
projectId: 'agrilink-vocpro-b37f9',
storageBucket: 'agrilink-vocpro-b37f9.appspot.com',
iosBundleId: 'com.pis.agrilinkVocpro',
);
}

View File

@ -1,15 +1,21 @@
import 'package:agrilink_vocpro/core/route/app_route.dart';
import 'package:agrilink_vocpro/domain/service/firebase_api.dart';
import 'package:agrilink_vocpro/features/auth/provider/auth_provider.dart';
import 'package:agrilink_vocpro/features/control/provider/control_provider.dart';
import 'package:agrilink_vocpro/features/dashboard/provider/dashboard_provider.dart';
import 'package:agrilink_vocpro/features/home/provider/home_provider.dart';
import 'package:agrilink_vocpro/features/setting/provider/setting_provider.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:provider/provider.dart';
void main() {
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp();
final firebaseApi = FirebaseApi();
await firebaseApi.initNotification();
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp, // Locks the app in portrait mode
]).then((_) {
@ -29,6 +35,7 @@ class MyApp extends StatelessWidget {
ChangeNotifierProvider(create: (context) => HomeProvider()),
ChangeNotifierProvider(create: (context) => DashboardProvider()),
ChangeNotifierProvider(create: (context) => ControlProvider()),
ChangeNotifierProvider(create: (context) => SettingProvider()),
],
child: ScreenUtilInit(
designSize: const Size(360, 800),
@ -44,7 +51,8 @@ class MyApp extends StatelessWidget {
),
routerConfig: AppRoute.router,
);
}),
},
),
);
}
}

View File

@ -5,10 +5,14 @@
import FlutterMacOS
import Foundation
import firebase_core
import firebase_messaging
import path_provider_foundation
import shared_preferences_foundation
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
}

View File

@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "5534e701a2c505fed1f0799e652dd6ae23bd4d2c4cf797220e5ced5764a7c1c2"
url: "https://pub.dev"
source: hosted
version: "1.3.44"
animated_segmented_tab_control:
dependency: "direct main"
description:
@ -9,6 +17,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.0.0"
args:
dependency: transitive
description:
name: args
sha256: bf9f5caeea8d8fe6721a9c358dd8a5c1947b27f1cfaa18b39c301273594919e6
url: "https://pub.dev"
source: hosted
version: "2.6.0"
async:
dependency: transitive
description:
@ -141,10 +157,58 @@ packages:
dependency: transitive
description:
name: file
sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c"
sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4
url: "https://pub.dev"
source: hosted
version: "7.0.0"
version: "7.0.1"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "51dfe2fbf3a984787a2e7b8592f2f05c986bfedd6fdacea3f9e0a7beb334de96"
url: "https://pub.dev"
source: hosted
version: "3.6.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: e30da58198a6d4b49d5bce4e852f985c32cb10db329ebef9473db2b9f09ce810
url: "https://pub.dev"
source: hosted
version: "5.3.0"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: f967a7138f5d2ffb1ce15950e2a382924239eaa521150a8f144af34e68b3b3e5
url: "https://pub.dev"
source: hosted
version: "2.18.1"
firebase_messaging:
dependency: "direct main"
description:
name: firebase_messaging
sha256: eb6e28a3a35deda61fe8634967c84215efc19133ba58d8e0fc6c9a2af2cba05e
url: "https://pub.dev"
source: hosted
version: "15.1.3"
firebase_messaging_platform_interface:
dependency: transitive
description:
name: firebase_messaging_platform_interface
sha256: b316c4ee10d93d32c033644207afc282d9b2b4372f3cf9c6022f3558b3873d2d
url: "https://pub.dev"
source: hosted
version: "4.5.46"
firebase_messaging_web:
dependency: transitive
description:
name: firebase_messaging_web
sha256: d7f0147a1a9fe4313168e20154a01fd5cf332898de1527d3930ff77b8c7f5387
url: "https://pub.dev"
source: hosted
version: "3.9.2"
fl_chart:
dependency: "direct main"
description:
@ -162,10 +226,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3f41d009ba7172d5ff9be5f6e6e6abb4300e263aab8866d2a0842ed2a70f8f0c"
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "5.0.0"
flutter_screenutil:
dependency: "direct main"
description:
@ -196,10 +260,10 @@ packages:
dependency: "direct main"
description:
name: go_router
sha256: "2ddb88e9ad56ae15ee144ed10e33886777eb5ca2509a914850a5faa7b52ff459"
sha256: "6f1b756f6e863259a99135ff3c95026c3cdca17d10ebef2bba2261a25ddc8bbc"
url: "https://pub.dev"
source: hosted
version: "14.2.7"
version: "14.3.0"
google_fonts:
dependency: "direct main"
description:
@ -208,6 +272,22 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.2.1"
google_identity_services_web:
dependency: transitive
description:
name: google_identity_services_web
sha256: "5be191523702ba8d7a01ca97c17fca096822ccf246b0a9f11923a6ded06199b6"
url: "https://pub.dev"
source: hosted
version: "0.3.1+4"
googleapis_auth:
dependency: "direct main"
description:
name: googleapis_auth
sha256: befd71383a955535060acde8792e7efc11d2fccd03dd1d3ec434e85b68775938
url: "https://pub.dev"
source: hosted
version: "1.6.0"
html:
dependency: transitive
description:
@ -217,7 +297,7 @@ packages:
source: hosted
version: "0.15.4"
http:
dependency: transitive
dependency: "direct main"
description:
name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
@ -240,6 +320,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.19.0"
jwt_decoder:
dependency: "direct main"
description:
name: jwt_decoder
sha256: "54774aebf83f2923b99e6416b4ea915d47af3bde56884eb622de85feabbc559f"
url: "https://pub.dev"
source: hosted
version: "2.0.1"
leak_tracker:
dependency: transitive
description:
@ -268,10 +356,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: "976c774dd944a42e83e2467f4cc670daef7eed6295b10b36ae8c85bcbf828235"
sha256: "3315600f3fb3b135be672bf4a178c55f274bebe368325ae18462c89ac1e3b413"
url: "https://pub.dev"
source: hosted
version: "4.0.0"
version: "5.0.0"
logging:
dependency: transitive
description:
@ -340,10 +428,10 @@ packages:
dependency: transitive
description:
name: path_provider_android
sha256: "6f01f8e37ec30b07bc424b4deabac37cacb1bc7e2e515ad74486039918a37eb7"
sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a
url: "https://pub.dev"
source: hosted
version: "2.2.10"
version: "2.2.12"
path_provider_foundation:
dependency: transitive
description:
@ -412,18 +500,18 @@ packages:
dependency: transitive
description:
name: shared_preferences_android
sha256: "480ba4345773f56acda9abf5f50bd966f581dac5d514e5fc4a18c62976bbba7e"
sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab"
url: "https://pub.dev"
source: hosted
version: "2.3.2"
version: "2.3.3"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: c4b35f6cb8f63c147312c054ce7c2254c8066745125264f0c88739c417fc9d9f
sha256: "07e050c7cd39bad516f8d64c455f04508d09df104be326d8c02551590a0d513d"
url: "https://pub.dev"
source: hosted
version: "2.5.2"
version: "2.5.3"
shared_preferences_linux:
dependency: transitive
description:
@ -561,18 +649,18 @@ packages:
dependency: transitive
description:
name: web
sha256: d43c1d6b787bf0afad444700ae7f4db8827f701bc61c255ac8d328c6f4d52062
sha256: cd3543bd5798f6ad290ea73d210f423502e71900302dde696f8bff84bf89a1cb
url: "https://pub.dev"
source: hosted
version: "1.0.0"
version: "1.1.0"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d
sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15"
url: "https://pub.dev"
source: hosted
version: "1.0.4"
version: "1.1.0"
sdks:
dart: ">=3.5.1 <4.0.0"
flutter: ">=3.22.0"
flutter: ">=3.24.0"

View File

@ -47,6 +47,11 @@ dependencies:
gauge_indicator: ^0.4.3
mqtt_client: ^10.5.1
shimmer: ^3.0.0
jwt_decoder: ^2.0.1
firebase_core: ^3.6.0
firebase_messaging: ^15.1.3
googleapis_auth: ^1.6.0
http: ^1.2.2
dev_dependencies:
flutter_test:
@ -57,7 +62,7 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^4.0.0
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
@ -73,6 +78,7 @@ flutter:
assets:
- assets/images/
- assets/icons/
- assets/json/
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see

View File

@ -6,6 +6,9 @@
#include "generated_plugin_registrant.h"
#include <firebase_core/firebase_core_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
}

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
firebase_core
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST