first commit

This commit is contained in:
aarsyah0 2025-07-16 09:51:27 +07:00
commit f8765ca76a
43 changed files with 3428 additions and 0 deletions

79
.gitignore vendored Normal file
View File

@ -0,0 +1,79 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json
# Android Studio will place build artifacts here
/android/app/debug
/android/app/profile
/android/app/release
# iOS/XCode related
**/ios/**/*.mode1v3
**/ios/**/*.mode2v3
**/ios/**/*.moved-aside
**/ios/**/*.pbxuser
**/ios/**/*.perspectivev3
**/ios/**/*sync/
**/ios/**/.sconsign.dblite
**/ios/**/.tags*
**/ios/**/.vagrant/
**/ios/**/DerivedData/
**/ios/**/Icon?
**/ios/**/Pods/
**/ios/**/.symlinks/
**/ios/**/profile
**/ios/**/xcuserdata
**/ios/.generated/
**/ios/Flutter/App.framework
**/ios/Flutter/Flutter.framework
**/ios/Flutter/Flutter.podspec
**/ios/Flutter/Generated.xcconfig
**/ios/Flutter/ephemeral
**/ios/Flutter/app.flx
**/ios/Flutter/app.zip
**/ios/Flutter/flutter_assets/
**/ios/ServiceDefinitions.json
**/ios/Runner/GeneratedPluginRegistrant.*
# Exceptions to above rules.
!**/ios/**/default.mode1v3
!**/ios/**/default.mode2v3
!**/ios/**/default.pbxuser
!**/ios/**/default.perspectivev3
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages

30
.metadata Normal file
View File

@ -0,0 +1,30 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "603104015dd692ea3403755b55d07813d5cf8965"
channel: "stable"
project_type: app
# Tracks metadata for the flutter migrate command
migration:
platforms:
- platform: root
create_revision: 603104015dd692ea3403755b55d07813d5cf8965
base_revision: 603104015dd692ea3403755b55d07813d5cf8965
- platform: web
create_revision: 603104015dd692ea3403755b55d07813d5cf8965
base_revision: 603104015dd692ea3403755b55d07813d5cf8965
# User provided section
# List of Local paths (relative to this file) that should be
# ignored by the migrate tool.
#
# Files that are not part of the templates will be ignored by default.
unmanaged_files:
- 'lib/main.dart'
- 'ios/Runner.xcodeproj/project.pbxproj'

16
README.md Normal file
View File

@ -0,0 +1,16 @@
# flutter_monitoringhujan
A new Flutter project.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook)
For help getting started with Flutter development, view the
[online documentation](https://docs.flutter.dev/), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

28
analysis_options.yaml Normal file
View File

@ -0,0 +1,28 @@
# This file configures the analyzer, which statically analyzes Dart code to
# check for errors, warnings, and lints.
#
# The issues identified by the analyzer are surfaced in the UI of Dart-enabled
# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be
# invoked from the command line by running `flutter analyze`.
# The following line activates a set of recommended lints for Flutter apps,
# packages, and plugins designed to encourage good coding practices.
include: package:flutter_lints/flutter.yaml
linter:
# The lint rules applied to this project can be customized in the
# section below to disable rules from the `package:flutter_lints/flutter.yaml`
# included above or to enable additional rules. A list of all available lints
# and their documentation is published at https://dart.dev/lints.
#
# Instead of disabling a lint rule for the entire project in the
# section below, it can also be suppressed for a single line of code
# or a specific dart file by using the `// ignore: name_of_lint` and
# `// ignore_for_file: name_of_lint` syntax on the line or in the file
# producing the lint.
rules:
# avoid_print: false # Uncomment to disable the `avoid_print` rule
# prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options

13
android/.gitignore vendored Normal file
View File

@ -0,0 +1,13 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks

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

@ -0,0 +1,82 @@
plugins {
id "com.android.application"
id "kotlin-android"
id "com.google.gms.google-services"
id "dev.flutter.flutter-gradle-plugin"
}
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new FileNotFoundException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
android {
namespace "com.example.flutter_monitoringhujan"
compileSdkVersion flutter.compileSdkVersion
ndkVersion flutter.ndkVersion
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = '1.8'
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
defaultConfig {
applicationId "com.example.flutter_monitoringhujan"
minSdkVersion 21
targetSdkVersion flutter.targetSdkVersion
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
multiDexEnabled true
}
buildTypes {
release {
signingConfig signingConfigs.debug
minifyEnabled false
shrinkResources false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
debug {
signingConfig signingConfigs.debug
minifyEnabled false
shrinkResources false
}
}
}
flutter {
source '../..'
}
dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'com.google.firebase:firebase-analytics'
implementation platform('com.google.firebase:firebase-bom:32.7.2')
implementation 'androidx.multidex:multidex:2.0.1'
}

View File

@ -0,0 +1,30 @@
{
"project_info": {
"project_number": "289397695071",
"firebase_url": "https://monitoring-hujan-default-rtdb.firebaseio.com",
"project_id": "monitoring-hujan",
"storage_bucket": "monitoring-hujan.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:289397695071:android:f6e034034faf366003b1ea",
"android_client_info": {
"package_name": "com.example.flutter_monitoringhujan"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyBoYp4GpkwF-aMPHVW1gs7PmOj5ucF4xZs"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

14
android/app/proguard-rules.pro vendored Normal file
View File

@ -0,0 +1,14 @@
# Flutter wrapper
-keep class io.flutter.app.** { *; }
-keep class io.flutter.plugin.** { *; }
-keep class io.flutter.util.** { *; }
-keep class io.flutter.view.** { *; }
-keep class io.flutter.** { *; }
-keep class io.flutter.plugins.** { *; }
# Firebase
-keep class com.google.firebase.** { *; }
-keep class com.google.android.gms.** { *; }
# Keep your model classes
-keep class com.example.flutter_monitoringhujan.** { *; }

View File

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

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<uses-permission android:name="android.permission.VIBRATE" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM"/>
<uses-permission android:name="android.permission.USE_EXACT_ALARM"/>
<application
android:label="Monitoring Hujan"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:allowBackup="true"
android:fullBackupContent="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<service
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</service>
<service
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationService"
android:exported="false"/>
<receiver
android:name="com.dexterous.flutterlocalnotifications.ScheduledNotificationBootReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
</intent-filter>
</receiver>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -0,0 +1,41 @@
package com.example.flutter_monitoringhujan
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugins.GeneratedPluginRegistrant
import android.os.Build
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.media.AudioAttributes
import android.net.Uri
import android.provider.Settings
class MainActivity: FlutterActivity() {
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val channel = NotificationChannel(
"monitoring_channel",
"Monitoring Hujan",
NotificationManager.IMPORTANCE_HIGH
).apply {
description = "Notifikasi untuk monitoring hujan"
enableLights(true)
enableVibration(true)
setShowBadge(true)
// Set suara notifikasi
val audioAttributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_NOTIFICATION)
.build()
setSound(Settings.System.DEFAULT_NOTIFICATION_URI, audioAttributes)
}
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
}
}

View File

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

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,3 @@
# This is a placeholder for the notification sound file.
# Please add your own notification sound file here (MP3 format)
# You can copy any short MP3 file and rename it to notification.mp3

View File

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

View File

@ -0,0 +1,5 @@
<resources>
<string name="app_name">Monitoring Hujan</string>
<string name="weather_channel_name">Weather Notifications</string>
<string name="weather_channel_description">Notifications for weather changes</string>
</resources>

View File

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

View File

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

32
android/build.gradle Normal file
View File

@ -0,0 +1,32 @@
buildscript {
ext.kotlin_version = '1.7.10'
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.3.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath 'com.google.gms:google-services:4.4.0'
}
}
allprojects {
repositories {
google()
mavenCentral()
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip

25
android/settings.gradle Normal file
View File

@ -0,0 +1,25 @@
pluginManagement {
def flutterSdkPath = {
def properties = new Properties()
file("local.properties").withInputStream { properties.load(it) }
def flutterSdkPath = properties.getProperty("flutter.sdk")
assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
return flutterSdkPath
}()
includeBuild("$flutterSdkPath/packages/flutter_tools/gradle")
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
include ":app"

3
devtools_options.yaml Normal file
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:

BIN
flutter_01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

116
kode_arduino_adib.ino Normal file
View File

@ -0,0 +1,116 @@
#include <WiFi.h>
#include <WiFiManager.h>
#include <HTTPClient.h>
#include <Wire.h>
#include <DHT.h>
#include <ESP32Servo.h>
#define DHT22_PIN 16
#define RAIN1_PIN 36
#define LDR_PIN 34
#define SERVO_PIN 18
const String firebaseHost = "https://monitoring-hujan-default-rtdb.firebaseio.com/";
DHT dht22(DHT22_PIN, DHT22);
Servo rainServo;
String firebaseGet(const String &child) {
HTTPClient http;
String url = firebaseHost + child + ".json";
http.begin(url);
int code = http.GET();
String payload = code == HTTP_CODE_OK ? http.getString() : "";
http.end();
return payload;
}
void firebasePut(const String &child, const String &val) {
HTTPClient http;
String url = firebaseHost + child + ".json";
http.begin(url);
http.addHeader("Content-Type", "application/json");
http.PUT(val);
http.end();
}
void firebasePatch(const String &json) {
HTTPClient http;
String url = firebaseHost + ".json";
http.begin(url);
http.addHeader("Content-Type", "application/json");
http.sendRequest("PATCH", json);
http.end();
}
void setup() {
Serial.begin(115200);
dht22.begin();
analogSetAttenuation(ADC_11db); // Agar ADC bisa baca hingga 3.3V
rainServo.setPeriodHertz(50);
rainServo.attach(SERVO_PIN, 500, 2400); // Attach servo
rainServo.write(0); // Servo awal tertutup
WiFiManager wm;
if (!wm.autoConnect("ESP32_AutoConnect", "password123")) {
ESP.restart();
}
Serial.println("WiFi connected");
}
void loop() {
float humi = dht22.readHumidity();
float tempC = dht22.readTemperature();
// LDR: Semakin terang, nilai semakin besar (01023)
int ldrRaw = analogRead(LDR_PIN);
int ldr = constrain(map(ldrRaw, 0, 4095, 0, 1023), 0, 1023);
// Rain sensor: Semakin basah, nilai semakin kecil (seperti Arduino)
int rainRaw = analogRead(RAIN1_PIN);
int rain = constrain(map(rainRaw, 0, 4095, 0, 1023), 0, 1023);
if (isnan(humi) || isnan(tempC)) {
delay(2000);
return;
}
// Logika kontrol servo
bool manual = (firebaseGet("modeManual") == "true");
int angle;
if (manual) {
angle = (firebaseGet("kontrolServo") == "true") ? 90 : 0;
} else {
angle = (rain < 500) ? 90 : 0; // Buka saat hujan
}
rainServo.write(angle);
delay(500);
firebasePut("servo", (angle == 0) ? "false" : "true");
// Logika status
String statusHujanText = (rain < 500) ? "Hujan" : "Tidak Hujan";
String statusCahayaText = (ldr < 700) ? "Terang" : "Gelap";
// Kirim data ke Firebase
String patchJson = "{";
patchJson += "\"humidity\":" + String(humi) + ",";
patchJson += "\"temperature\":" + String(tempC) + ",";
patchJson += "\"rain\":" + String(rain) + ",";
patchJson += "\"ldr\":" + String(ldr) + ",";
patchJson += "\"statusHujan\":\"" + statusHujanText + "\",";
patchJson += "\"statusCahaya\":\"" + statusCahayaText + "\"";
patchJson += "}";
firebasePatch(patchJson);
// Debug ke Serial Monitor
Serial.printf("H:%.1f T:%.1f R:%d L:%d | Hujan:%s | Cahaya:%s | Mode:%s | Servo:%d\n",
humi, tempC, rain, ldr,
statusHujanText.c_str(),
statusCahayaText.c_str(),
manual ? "MANUAL" : "OTOMATIS",
angle);
delay(5000); // Delay 5 detik antar pengiriman
}

435
lib/background_service.dart Normal file
View File

@ -0,0 +1,435 @@
// lib/services/background_service.dart
import 'dart:async';
import 'dart:io';
import 'dart:ui';
import 'package:flutter/foundation.dart' show kIsWeb, kDebugMode;
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:flutter_background_service_android/flutter_background_service_android.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:workmanager/workmanager.dart';
/// Inisialisasi plugin notifikasi
final FlutterLocalNotificationsPlugin _notifPlugin =
FlutterLocalNotificationsPlugin();
/// ID channel notifikasi
const String _channelId = 'rain_notification_channel';
/// Nama channel notifikasi
const String _channelName = 'Rain Notifications';
/// Deskripsi channel notifikasi
const String _channelDesc = 'Notifications for rain sensor updates';
/// Channel Android
const _channelIdAndroid = 'monitoring_hujan_channel';
const _channelNameAndroid = 'Monitoring Hujan';
const _channelDescAndroid = 'Notifikasi status hujan';
Future<void> saveToFirestoreIfNotDuplicate({
required double suhu,
required double kelembaban,
required double cahaya,
required double rainValue,
required String statusHujan,
required String statusCahaya,
}) async {
final now = DateTime.now();
final thirtySecondsAgo = now.subtract(const Duration(seconds: 30));
final query = await FirebaseFirestore.instance
.collection('sensor_history')
.where('timestamp', isGreaterThan: Timestamp.fromDate(thirtySecondsAgo))
.get();
if (query.docs.isEmpty) {
await FirebaseFirestore.instance.collection('sensor_history').add({
'temperature': suhu,
'humidity': kelembaban,
'light': cahaya,
'rain': rainValue,
'rain_status': statusHujan,
'weather_status': statusCahaya,
'timestamp': FieldValue.serverTimestamp(),
});
print('✅ Data tersimpan ke Firestore (background)');
} else {
print('⏩ Data sudah ada, tidak disimpan ulang (anti duplikat, background)');
}
}
@pragma('vm:entry-point')
Future<void> initializeService() async {
if (kIsWeb) return;
// 1) Setup channel & plugin notifikasi
const androidSetup = AndroidInitializationSettings('@mipmap/ic_launcher');
final initSettings = InitializationSettings(android: androidSetup);
await _notifPlugin.initialize(initSettings);
// Hapus channel lama jika ada
await _notifPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.deleteNotificationChannel(_channelIdAndroid);
// Buat channel baru dengan deskripsi yang diperbarui
await _notifPlugin
.resolvePlatformSpecificImplementation<
AndroidFlutterLocalNotificationsPlugin>()
?.createNotificationChannel(
const AndroidNotificationChannel(
_channelIdAndroid,
_channelNameAndroid,
description: _channelDescAndroid,
importance: Importance.max,
enableVibration: true,
enableLights: true,
showBadge: true,
),
);
// 2) Konfigurasikan service
final service = FlutterBackgroundService();
await service.configure(
androidConfiguration: AndroidConfiguration(
onStart: onStart,
autoStart: true,
isForegroundMode: true,
notificationChannelId: _channelIdAndroid,
initialNotificationTitle: 'Monitoring Hujan',
initialNotificationContent: 'Memulai monitoring...',
foregroundServiceNotificationId: 888,
),
iosConfiguration: IosConfiguration(
autoStart: true,
onForeground: onStart,
onBackground: onIosBackground,
),
);
// Mulai service
await service.startService();
}
@pragma('vm:entry-point')
Future<bool> onIosBackground(ServiceInstance service) async {
return true;
}
@pragma('vm:entry-point')
void onStart(ServiceInstance service) async {
if (kIsWeb) return;
print('🚀 Background service dimulai...');
// Inisialisasi WorkManager
await Workmanager().initialize(callbackDispatcher);
// Inisialisasi SharedPreferences
final prefs = await SharedPreferences.getInstance();
int saveInterval = prefs.getInt('saveInterval') ?? 1; // menit
int remainingSeconds =
prefs.getInt('remainingSeconds') ?? (saveInterval * 60);
DateTime? lastSaveTime = prefs.getString('lastSaveTime') != null
? DateTime.parse(prefs.getString('lastSaveTime')!)
: null;
// --- Tambahkan variabel untuk menyimpan rain sebelumnya ---
double? previousRainValue;
// Inisialisasi Firebase jika belum ada
try {
if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(
options: const FirebaseOptions(
apiKey: 'AIzaSyBoYp4GpkwF-aMPHVW1gs7PmOj5ucF4xZs',
appId: '1:289397695071:android:f6e034034faf366003b1ea',
messagingSenderId: '289397695071',
projectId: 'monitoring-hujan',
databaseURL: 'https://monitoring-hujan-default-rtdb.firebaseio.com',
),
);
print('✅ Firebase berhasil diinisialisasi di background service');
// Ambil interval dari Firestore
try {
final settingsDoc = await FirebaseFirestore.instance
.collection('settings')
.doc('save_interval')
.get();
if (settingsDoc.exists) {
saveInterval = settingsDoc.data()?['interval'] ?? 1;
await prefs.setInt('saveInterval', saveInterval);
print(
'✅ Interval berhasil diambil dari Firestore: $saveInterval menit');
} else {
// Buat dokumen settings jika belum ada
await FirebaseFirestore.instance
.collection('settings')
.doc('save_interval')
.set({
'interval': saveInterval,
'last_updated': FieldValue.serverTimestamp(),
});
print(
'✅ Dokumen settings dibuat dengan interval default: $saveInterval menit');
}
} catch (e) {
print('⚠️ Gagal mengambil interval dari Firestore: $e');
}
}
} catch (e) {
print('❌ Error inisialisasi Firebase: $e');
if (service is AndroidServiceInstance) {
await service.setForegroundNotificationInfo(
title: 'Monitoring Hujan',
content: 'Error inisialisasi aplikasi',
);
}
return;
}
// Hitung sisa waktu jika ada lastSaveTime
if (lastSaveTime != null) {
final now = DateTime.now();
final difference = now.difference(lastSaveTime).inSeconds;
remainingSeconds = (saveInterval * 60) - difference;
if (remainingSeconds < 0) remainingSeconds = 0;
}
// Pastikan ini tetap demi foreground service
if (service is AndroidServiceInstance) {
await service.setAsForegroundService();
print('✅ Service diatur sebagai foreground service');
}
// Update notifikasi awal
if (service is AndroidServiceInstance) {
final minutes = (remainingSeconds ~/ 60).toString().padLeft(2, '0');
final seconds = (remainingSeconds % 60).toString().padLeft(2, '0');
await service.setForegroundNotificationInfo(
title: 'Monitoring Hujan',
content: 'Mengambil data sensor... Next save: $minutes:$seconds',
);
print('📱 Notifikasi awal diperbarui');
}
// Timer untuk menyimpan data ke history
Timer? saveTimer;
// Fungsi untuk mencoba koneksi ulang
Future<void> retryConnection() async {
try {
final dbRef = FirebaseDatabase.instance.ref();
await dbRef.get();
print('✅ Koneksi Firebase berhasil');
return;
} catch (e) {
print('❌ Gagal koneksi: $e');
if (service is AndroidServiceInstance) {
await service.setForegroundNotificationInfo(
title: 'Monitoring Hujan',
content: 'Mencoba menghubungkan kembali...',
);
}
// Tunggu 5 detik sebelum mencoba lagi
await Future.delayed(const Duration(seconds: 5));
return retryConnection();
}
}
// Fungsi untuk setup listener
void setupListener(DatabaseReference dbRef) {
dbRef.onValue.listen(
(event) async {
try {
if (event.snapshot.value != null) {
final data = event.snapshot.value as Map<dynamic, dynamic>;
final suhu = double.parse(data['temperature']?.toString() ?? '0');
final kelembaban =
double.parse(data['humidity']?.toString() ?? '0');
final cahaya = double.parse(data['ldr']?.toString() ?? '0');
final rainValue = double.parse(data['rain']?.toString() ?? '0');
final statusHujan =
data['statusHujan']?.toString() ?? _getStatusHujan(rainValue);
final statusCahaya =
data['statusCahaya']?.toString() ?? _getStatusCuaca(cahaya);
print('📊 Data diperbarui:');
print(' - Suhu: $suhu°C');
print(' - Kelembaban: $kelembaban%');
print(' - Cahaya: $cahaya');
print(' - Rain value: $rainValue');
print(' - Status Hujan: $statusHujan');
print(' - Status Cahaya: $statusCahaya');
// Simpan ke Firestore setiap interval
if (remainingSeconds <= 0) {
await saveToFirestoreIfNotDuplicate(
suhu: suhu,
kelembaban: kelembaban,
cahaya: cahaya,
rainValue: rainValue,
statusHujan: statusHujan,
statusCahaya: statusCahaya,
);
}
// --- Update notifikasi hanya jika rain berubah ---
if (previousRainValue == null || previousRainValue != rainValue) {
previousRainValue = rainValue;
if (service is AndroidServiceInstance) {
final minutes =
(remainingSeconds ~/ 60).toString().padLeft(2, '0');
final seconds =
(remainingSeconds % 60).toString().padLeft(2, '0');
await service.setForegroundNotificationInfo(
title: 'Status Hujan: ${_getStatusHujan(rainValue)}',
content: 'Curah Hujan: ${rainValue.toStringAsFixed(0)}',
);
print('🔔 Notifikasi diperbarui karena rain berubah');
}
}
// --- END ---
}
} catch (e) {
print('❌ Error memproses data: $e');
if (service is AndroidServiceInstance) {
await service.setForegroundNotificationInfo(
title: 'Monitoring Hujan',
content: 'Error memproses data sensor',
);
}
}
},
onError: (error) {
print('❌ Error pada listener: $error');
if (service is AndroidServiceInstance) {
service.setForegroundNotificationInfo(
title: 'Monitoring Hujan',
content: 'Mencoba menghubungkan kembali...',
);
}
// Coba setup ulang listener setelah error
Future.delayed(const Duration(seconds: 5), () {
setupListener(FirebaseDatabase.instance.ref());
});
},
cancelOnError: false,
);
}
// Mulai proses koneksi
try {
print('🔄 Mencoba menghubungkan ke Firebase...');
await retryConnection();
// Setup listener setelah koneksi berhasil
final dbRef = FirebaseDatabase.instance.ref();
setupListener(dbRef);
// Setup timer untuk countdown
saveTimer = Timer.periodic(const Duration(seconds: 1), (timer) async {
if (remainingSeconds > 0) {
remainingSeconds--;
await prefs.setInt('remainingSeconds', remainingSeconds);
}
});
} catch (e) {
print('❌ Error fatal: $e');
if (service is AndroidServiceInstance) {
await service.setForegroundNotificationInfo(
title: 'Monitoring Hujan',
content: 'Error koneksi ke server',
);
}
// Coba koneksi ulang setelah error fatal
Future.delayed(const Duration(seconds: 5), () {
onStart(service);
});
}
// Cleanup saat service dihentikan
service.on('stopService').listen((event) {
saveTimer?.cancel();
print('🛑 Service dihentikan');
});
}
/// Helper menentukan status hujan
String _getStatusHujan(double rainValue) {
// Status hujan berdasarkan nilai sensor
return (rainValue < 500) ? "Hujan" : "Tidak Hujan";
}
/// Helper menentukan status cuaca
String _getStatusCuaca(double cahaya) {
// Status cahaya berdasarkan nilai LDR
return (cahaya < 700) ? "Terang" : "Gelap";
}
Future<void> _updateNotification(
String title, String content, String status) async {
// Implementasi untuk mengupdate notifikasi
}
@pragma('vm:entry-point')
void callbackDispatcher() {
Workmanager().executeTask((task, inputData) async {
print('🔄 Background task dijalankan: $task');
if (task == 'saveData') {
try {
// Inisialisasi Firebase jika belum ada
if (Firebase.apps.isEmpty) {
await Firebase.initializeApp(
options: const FirebaseOptions(
apiKey: 'AIzaSyBoYp4GpkwF-aMPHVW1gs7PmOj5ucF4xZs',
appId: '1:289397695071:android:f6e034034faf366003b1ea',
messagingSenderId: '289397695071',
projectId: 'monitoring-hujan',
databaseURL:
'https://monitoring-hujan-default-rtdb.firebaseio.com',
),
);
}
// Ambil data dari Firebase
final dbRef = FirebaseDatabase.instance.ref();
final snapshot = await dbRef.get();
if (snapshot.value != null) {
final data = snapshot.value as Map<dynamic, dynamic>;
final suhu = double.parse(data['temperature']?.toString() ?? '0');
final kelembaban = double.parse(data['humidity']?.toString() ?? '0');
final cahaya = double.parse(data['ldr']?.toString() ?? '0');
final rainValue = double.parse(data['rain']?.toString() ?? '0');
final statusHujan =
data['statusHujan']?.toString() ?? _getStatusHujan(rainValue);
final statusCahaya =
data['statusCahaya']?.toString() ?? _getStatusCuaca(cahaya);
// Simpan ke Firestore dengan anti-duplikat
await saveToFirestoreIfNotDuplicate(
suhu: suhu,
kelembaban: kelembaban,
cahaya: cahaya,
rainValue: rainValue,
statusHujan: statusHujan,
statusCahaya: statusCahaya,
);
print('✅ Data berhasil disimpan dari background task');
}
} catch (e) {
print('❌ Error dalam background task: $e');
}
}
return Future.value(true);
});
}

740
lib/history_page.dart Normal file
View File

@ -0,0 +1,740 @@
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:intl/intl.dart';
import 'dart:async';
class HistoryPage extends StatefulWidget {
const HistoryPage({super.key});
@override
State<HistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> {
DateTime? _selectedDate;
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
Timer? _refreshTimer;
int _refreshInterval = 1; // Default 1 menit
final DateFormat _dateFormat = DateFormat('HH:mm:ss');
Map<String, List<QueryDocumentSnapshot>> _groupedData = {};
Set<String> _expandedGroups = {};
// Tambahkan state untuk filter sensor
String _selectedSensor = 'Semua';
final List<String> _sensorOptions = [
'Semua',
'Suhu',
'Kelembaban',
'Cahaya',
'Hujan',
'Status Cuaca',
];
@override
void initState() {
super.initState();
_startRefreshTimer();
}
@override
void dispose() {
_refreshTimer?.cancel();
super.dispose();
}
void _startRefreshTimer() {
_refreshTimer?.cancel(); // Cancel existing timer if any
_refreshTimer =
Timer.periodic(Duration(minutes: _refreshInterval), (timer) {
setState(() {
// Force rebuild to refresh data
});
});
}
// Fungsi untuk mengelompokkan data berdasarkan waktu
Map<String, List<QueryDocumentSnapshot>> _groupDataByTime(
List<QueryDocumentSnapshot> docs) {
Map<String, List<QueryDocumentSnapshot>> grouped = {};
for (var doc in docs) {
final data = doc.data() as Map<String, dynamic>;
final timestamp = (data['timestamp'] as Timestamp).toDate();
// Kelompokkan berdasarkan waktu lengkap (dd/MM/yyyy HH:mm)
String groupKey = DateFormat('dd/MM/yyyy HH:mm').format(timestamp);
if (!grouped.containsKey(groupKey)) {
grouped[groupKey] = [];
}
grouped[groupKey]!.add(doc);
}
return grouped;
}
// Fungsi untuk menampilkan detail data
void _showDataDetail(QueryDocumentSnapshot doc) {
print('TAP DETAIL');
final data = doc.data() as Map<String, dynamic>;
debugPrint('Data detail: ' + data.toString());
final timestamp = (data['timestamp'] as Timestamp?)?.toDate();
final suhu = (data['temperature'] ?? '-') as dynamic;
final kelembaban = (data['humidity'] ?? '-') as dynamic;
final cahaya = (data['light'] ?? '-') as dynamic;
final rainValue = (data['rain'] ?? '-') as dynamic;
final rainStatus = data['rain_status'] as String? ?? '-';
final weatherStatus = data['weather_status'] as String? ??
(cahaya is num ? _calculateWeatherStatus(cahaya.toDouble()) : '-');
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Row(
children: [
const Icon(Icons.info_outline, color: Colors.blue),
const SizedBox(width: 8),
const Text('Detail Data Sensor'),
],
),
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Tampilkan isi data Map untuk debug
if (data.isEmpty)
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Data kosong: \\${doc.id}',
style: const TextStyle(color: Colors.red),
),
)
else
Padding(
padding: const EdgeInsets.only(bottom: 8),
child: Text(
'Data: \\${data.toString()}',
style: const TextStyle(fontSize: 12, color: Colors.grey),
),
),
_buildDetailRow(
'Waktu',
timestamp != null
? DateFormat('dd/MM/yyyy HH:mm:ss').format(timestamp)
: '-'),
const Divider(),
_buildDetailRow(
'Suhu',
suhu is num
? '${suhu.toDouble().toStringAsFixed(1)}°C'
: '-'),
_buildDetailRow('Status Suhu',
suhu is num ? _getStatusSuhu(suhu.toDouble()) : '-'),
const Divider(),
_buildDetailRow(
'Kelembaban',
kelembaban is num
? '${kelembaban.toDouble().toStringAsFixed(1)}%'
: '-'),
_buildDetailRow(
'Status Kelembaban',
kelembaban is num
? _getStatusKelembaban(kelembaban.toDouble())
: '-'),
const Divider(),
_buildDetailRow(
'Intensitas Cahaya',
cahaya is num
? '${cahaya.toDouble().toStringAsFixed(0)} lux'
: '-'),
_buildDetailRow('Status Cuaca', weatherStatus),
const Divider(),
_buildDetailRow('Curah Hujan',
rainValue is num ? '${rainValue.toStringAsFixed(0)}' : '-'),
_buildDetailRow(
'Status Hujan',
rainValue is num
? _getStatusHujan(rainValue.toDouble())
: '-'),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Tutup'),
),
],
),
);
print('SHOW DIALOG DIPANGGIL');
}
Widget _buildDetailRow(String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 120,
child: Text(
label,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
const Text(': ', style: TextStyle(fontWeight: FontWeight.bold)),
Expanded(
child: Text(
value,
style: const TextStyle(fontSize: 14),
),
),
],
),
);
}
Widget _buildSensorRow(String label, String value, String status) {
return Row(
children: [
Expanded(
flex: 2,
child: Text(
label,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
Expanded(
flex: 1,
child: Text(
value,
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
),
),
),
if (status.isNotEmpty)
Expanded(
flex: 1,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: _getStatusColor(status).withOpacity(0.2),
borderRadius: BorderRadius.circular(8),
),
child: Text(
status,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: _getStatusColor(status),
),
textAlign: TextAlign.center,
),
),
),
],
);
}
// void _showRefreshIntervalDialog() {
// showDialog(
// context: context,
// builder: (context) => AlertDialog(
// title: const Text('Atur Interval Refresh'),
// content: Column(
// mainAxisSize: MainAxisSize.min,
// children: [
// const Text('Pilih interval refresh data (dalam menit):'),
// const SizedBox(height: 16),
// DropdownButton<int>(
// value: _refreshInterval,
// isExpanded: true,
// items: const [
// DropdownMenuItem(value: 1, child: Text('1 menit')),
// DropdownMenuItem(value: 2, child: Text('2 menit')),
// DropdownMenuItem(value: 5, child: Text('5 menit')),
// DropdownMenuItem(value: 10, child: Text('10 menit')),
// DropdownMenuItem(value: 15, child: Text('15 menit')),
// DropdownMenuItem(value: 30, child: Text('30 menit')),
// ],
// onChanged: (value) {
// if (value != null) {
// setState(() {
// _refreshInterval = value;
// });
// _startRefreshTimer();
// Navigator.pop(context);
// }
// },
// ),
// ],
// ),
// actions: [
// TextButton(
// onPressed: () => Navigator.pop(context),
// child: const Text('Batal'),
// ),
// ],
// ),
// );
// }
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Row(
children: [
Text(
'Riwayat Data',
style:
TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
],
),
backgroundColor: Theme.of(context).colorScheme.primary,
actions: [
if (_selectedDate != null)
IconButton(
icon: const Icon(Icons.clear, color: Colors.white),
onPressed: () {
setState(() {
_selectedDate = null;
});
},
tooltip: 'Hapus Filter Tanggal',
),
IconButton(
icon: const Icon(Icons.calendar_today, color: Colors.white),
onPressed: () => _selectDate(context),
tooltip: 'Pilih Tanggal',
),
// IconButton(
// icon: const Icon(Icons.timer),
// onPressed: _showRefreshIntervalDialog,
// tooltip: 'Atur Interval Refresh',
// ),
],
),
body: Column(
children: [
// Expanded agar StreamBuilder tetap memenuhi sisa layar
Expanded(
child: StreamBuilder<QuerySnapshot>(
stream: _firestore
.collection('sensor_history')
.orderBy('timestamp', descending: true)
.where('timestamp',
isGreaterThanOrEqualTo: _selectedDate != null
? Timestamp.fromDate(DateTime(_selectedDate!.year,
_selectedDate!.month, _selectedDate!.day))
: Timestamp.fromDate(
DateTime.now().subtract(const Duration(days: 1))))
.where('timestamp',
isLessThan: _selectedDate != null
? Timestamp.fromDate(DateTime(_selectedDate!.year,
_selectedDate!.month, _selectedDate!.day + 1))
: Timestamp.fromDate(
DateTime.now().add(const Duration(days: 1))))
.limit(100) // Batasi jumlah data yang ditampilkan
.snapshots(includeMetadataChanges: true),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
const SizedBox(height: 16),
Text(
'Error: ${snapshot.error}',
style: const TextStyle(color: Colors.red),
),
],
),
);
}
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (!snapshot.hasData || snapshot.data!.docs.isEmpty) {
return const Center(
child: Text(
'Tidak ada data riwayat',
style: TextStyle(fontSize: 16),
),
);
}
// Filter data berdasarkan tanggal yang dipilih
final filteredDocs = _selectedDate != null
? snapshot.data!.docs.where((doc) {
final data = doc.data() as Map<String, dynamic>;
final timestamp = data['timestamp'] as Timestamp;
final docDate = timestamp.toDate();
return docDate.year == _selectedDate!.year &&
docDate.month == _selectedDate!.month &&
docDate.day == _selectedDate!.day;
}).toList()
: snapshot.data!.docs;
if (filteredDocs.isEmpty) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.calendar_today,
size: 60,
color: Colors.grey,
),
const SizedBox(height: 16),
Text(
'Tidak ada data untuk tanggal ${_selectedDate != null ? DateFormat('EEEE, dd/MM/yyyy').format(_selectedDate!) : 'yang dipilih'}',
style: const TextStyle(fontSize: 16),
textAlign: TextAlign.center,
),
],
),
);
}
// Tampilkan waktu update terakhir
final lastUpdate = (filteredDocs.first.data()
as Map<String, dynamic>)['timestamp'] as Timestamp;
final lastUpdateTime = lastUpdate.toDate();
return Column(
children: [
Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Text(
'Terakhir diperbarui: ${DateFormat('HH:mm:ss').format(lastUpdateTime)}',
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
if (_selectedDate != null) ...[
const SizedBox(height: 4),
Text(
'Tanggal: ${DateFormat('EEEE, dd/MM/yyyy').format(_selectedDate!)}',
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
],
),
),
Expanded(
child: ListView.builder(
itemCount: filteredDocs.length,
itemBuilder: (context, index) {
final doc = filteredDocs[index];
final data = doc.data() as Map<String, dynamic>;
final timestamp =
(data['timestamp'] as Timestamp).toDate();
final suhu = (data['temperature'] ?? 0.0) as num;
final kelembaban = (data['humidity'] ?? 0.0) as num;
final cahaya = (data['light'] ?? 0.0) as num;
final rainValue = (data['rain'] ?? 0.0) as num;
final weatherStatus =
_getStatusCuaca(cahaya.toDouble());
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 12),
child: InkWell(
borderRadius: BorderRadius.circular(24),
splashColor: Colors.blue.withOpacity(0.1),
highlightColor: Colors.blue.withOpacity(0.05),
onTap: () {}, // hanya untuk efek ripple
child: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.blue.shade50,
Colors.blue.shade100.withOpacity(0.7),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: Colors.blueGrey.withOpacity(0.10),
blurRadius: 16,
offset: const Offset(0, 8),
),
],
),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 22, vertical: 22),
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(Icons.access_time,
color: Colors.blue, size: 22),
const SizedBox(width: 12),
Text(
DateFormat('dd/MM/yyyy HH:mm:ss')
.format(timestamp),
style: const TextStyle(
fontWeight: FontWeight.w900,
fontSize: 18,
color: Colors.blue,
letterSpacing: 0.5,
),
),
],
),
const SizedBox(height: 18),
Row(
children: [
const Icon(Icons.thermostat,
color: Colors.redAccent,
size: 20),
const SizedBox(width: 8),
Expanded(
child: _buildDetailRow('Suhu',
'${suhu.toDouble().toStringAsFixed(1)}°C')),
const SizedBox(width: 8),
_statusChip(
_getStatusSuhu(suhu.toDouble()),
_getStatusColor(_getStatusSuhu(
suhu.toDouble())),
),
],
),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.water_drop,
color: Colors.blueAccent,
size: 20),
const SizedBox(width: 8),
Expanded(
child: _buildDetailRow(
'Kelembaban',
'${kelembaban.toDouble().toStringAsFixed(1)}%')),
const SizedBox(width: 8),
_statusChip(
_getStatusKelembaban(
kelembaban.toDouble()),
_getStatusColor(
_getStatusKelembaban(
kelembaban.toDouble()))),
],
),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.wb_sunny,
color: Colors.orange, size: 20),
const SizedBox(width: 8),
Expanded(
child: _buildDetailRow('Cahaya',
'${cahaya.toDouble().toStringAsFixed(0)} lux')),
const SizedBox(width: 8),
_statusChip(weatherStatus,
_getStatusColor(weatherStatus)),
],
),
const SizedBox(height: 8),
Row(
children: [
const Icon(Icons.grain,
color: Colors.indigo, size: 20),
const SizedBox(width: 8),
Expanded(
child: _buildDetailRow(
'Curah Hujan',
'${rainValue.toStringAsFixed(0)}')),
const SizedBox(width: 8),
_statusChip(
_getStatusHujan(
rainValue.toDouble()),
_getStatusColor(_getStatusHujan(
rainValue.toDouble()))),
],
),
],
),
),
),
),
);
},
),
),
],
);
},
),
),
],
),
);
}
String _getStatusHujan(double kadar) {
// Status hujan berdasarkan nilai sensor
return (kadar < 500) ? "Hujan" : "Tidak Hujan";
}
String _getStatusCuaca(double cahaya) {
// Status cahaya berdasarkan nilai LDR
return (cahaya < 700) ? "Terang" : "Gelap";
}
Color _getStatusColor(String status) {
// Warna untuk setiap status sensor
switch (status) {
case 'Hujan':
return Colors.blue.shade700;
case 'Tidak Hujan':
return Colors.orange;
case 'Terang':
return Colors.orange;
case 'Gelap':
return Colors.grey.shade800;
case 'Dingin':
return Colors.blue.shade700;
case 'Sejuk':
return Colors.blue.shade400;
case 'Normal':
return Colors.green;
case 'Hangat':
return Colors.orange.shade700;
case 'Panas':
return Colors.red;
case 'Kering':
return Colors.orange;
case 'Basah':
return Colors.blue;
default:
return Colors.grey;
}
}
String _calculateWeatherStatus(double cahaya) {
// Status cuaca berdasarkan nilai LDR
return (cahaya < 700) ? "Terang" : "Gelap";
}
Color _getWeatherStatusColor(String status) {
switch (status) {
case 'Terang':
return Colors.orange;
case 'Gelap':
return Colors.grey.shade800;
default:
return Colors.grey;
}
}
String _getStatusSuhu(double suhu) {
// Status suhu berdasarkan nilai sensor
if (suhu <= 20) return 'Dingin';
if (suhu <= 23) return 'Sejuk';
if (suhu <= 26) return 'Normal';
if (suhu <= 27) return 'Hangat';
return 'Panas';
}
String _getStatusKelembaban(double kelembaban) {
// Status kelembaban berdasarkan nilai sensor
if (kelembaban < 40) return 'Kering';
if (kelembaban > 80) return 'Basah';
return 'Normal';
}
Color _getStatusSuhuColor(String status) {
// Warna untuk setiap status suhu
switch (status) {
case 'Dingin':
return Colors.blue.shade700;
case 'Sejuk':
return Colors.blue.shade400;
case 'Normal':
return Colors.orange;
case 'Hangat':
return Colors.orange.shade700;
case 'Panas':
return Colors.red;
default:
return Colors.grey;
}
}
Future<void> _selectDate(BuildContext context) async {
final DateTime? picked = await showDatePicker(
context: context,
initialDate: _selectedDate ?? DateTime.now(),
firstDate: DateTime(2020),
lastDate: DateTime.now(),
builder: (context, child) {
return Theme(
data: Theme.of(context).copyWith(
colorScheme: ColorScheme.light(
primary: Theme.of(context).colorScheme.primary,
onPrimary: Colors.white,
surface: Colors.white,
onSurface: Colors.black,
),
),
child: child!,
);
},
);
if (picked != null) {
setState(() {
_selectedDate = picked;
});
}
}
Widget _statusChip(String label, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withOpacity(0.15),
borderRadius: BorderRadius.circular(12),
),
child: Text(
label,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: 12,
),
),
);
}
}

783
lib/main.dart Normal file
View File

@ -0,0 +1,783 @@
import 'dart:async';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:firebase_core/firebase_core.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_localizations/flutter_localizations.dart';
import 'history_page.dart';
import 'background_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Inisialisasi Firebase hanya jika belum ada instance
if (Firebase.apps.isEmpty) {
try {
await Firebase.initializeApp(
options: const FirebaseOptions(
apiKey: 'AIzaSyBoYp4GpkwF-aMPHVW1gs7PmOj5ucF4xZs',
appId: '1:289397695071:android:f6e034034faf366003b1ea',
messagingSenderId: '289397695071',
projectId: 'monitoring-hujan',
databaseURL: 'https://monitoring-hujan-default-rtdb.firebaseio.com',
),
);
print('✅ Firebase berhasil diinisialisasi di main');
} catch (e) {
print('❌ Error inisialisasi Firebase di main: $e');
}
} else {
print(' Firebase sudah diinisialisasi sebelumnya');
}
// Inisialisasi background service
try {
await initializeService();
print('✅ Background service berhasil diinisialisasi');
} catch (e) {
print('❌ Error inisialisasi background service: $e');
}
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Monitoring Kolam',
localizationsDelegates: const [
GlobalMaterialLocalizations.delegate,
GlobalWidgetsLocalizations.delegate,
GlobalCupertinoLocalizations.delegate,
],
supportedLocales: const [
Locale('id', 'ID'),
Locale('en', 'US'),
],
locale: const Locale('id', 'ID'),
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF2196F3),
primary: const Color(0xFF2196F3),
secondary: const Color(0xFF03A9F4),
),
useMaterial3: true,
),
home: const MainPage(),
);
}
}
class MainPage extends StatefulWidget {
const MainPage({super.key});
@override
State<MainPage> createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
int _currentIndex = 0;
late List<Widget> _pages;
Timer? _saveTimer;
Timer? _countdownTimer;
int _remainingSeconds = 60;
Duration _saveInterval = const Duration(minutes: 1);
@override
void initState() {
super.initState();
_initializeApp();
_startSaveTimer();
_startCountdownTimer();
}
Future<void> _initializeApp() async {
_pages = [
MonitoringPage(
key: UniqueKey(),
onDataUpdate: (s, k, c, ka) => _updateSensorData(s, k, c, ka),
onIntervalChange: _changeSaveInterval,
currentInterval: _saveInterval,
),
const HistoryPage(),
];
setState(() {});
}
void _changeSaveInterval(Duration newInterval) {
print(
'🔄 Mengubah interval dari ${_saveInterval.inSeconds}s ke ${newInterval.inSeconds}s');
setState(() {
_saveInterval = newInterval;
_startSaveTimer();
});
print('✅ Interval berhasil diubah');
}
void _updateSensorData(
double suhu, double kelembaban, double cahaya, double rainVal) {
// No need to update state here as _saveToFirestore is handled by background service
}
void _startSaveTimer() {
_saveTimer?.cancel();
_remainingSeconds = _saveInterval.inSeconds;
print('⏰ Timer dimulai dengan interval: ${_saveInterval.inSeconds} detik');
_saveTimer = Timer.periodic(_saveInterval, (timer) async {
print('⏰ Timer terpanggil - mengirim data ke Firestore...');
await _saveFromRealtimeToFirestore();
setState(() {
_remainingSeconds = _saveInterval.inSeconds;
});
print('⏰ Timer selesai - menunggu interval berikutnya');
});
}
void _startCountdownTimer() {
_countdownTimer?.cancel();
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
if (!mounted) return;
setState(() {
if (_remainingSeconds > 0) {
_remainingSeconds--;
} else {
_remainingSeconds = _saveInterval.inSeconds;
}
});
});
}
Future<void> _saveFromRealtimeToFirestore() async {
try {
print('🔄 Memulai pengiriman data ke Firestore...');
final dbRef = FirebaseDatabase.instance.ref();
final snapshot = await dbRef.get();
if (snapshot.value != null) {
final data = snapshot.value as Map<dynamic, dynamic>;
final suhu = double.parse(data['temperature']?.toString() ?? '0');
final kelembaban = double.parse(data['humidity']?.toString() ?? '0');
final cahaya = double.parse(data['ldr']?.toString() ?? '0');
final rainValue = double.parse(data['rain']?.toString() ?? '0');
final statusHujan = data['statusHujan']?.toString() ?? '';
final statusCahaya = data['statusCahaya']?.toString() ?? '';
print(
'📊 Data sensor: Suhu=$suhu, Kelembaban=$kelembaban, Cahaya=$cahaya, Hujan=$rainValue');
final docRef =
await FirebaseFirestore.instance.collection('sensor_history').add({
'temperature': suhu,
'humidity': kelembaban,
'light': cahaya,
'rain': rainValue,
'rain_status': statusHujan,
'weather_status': statusCahaya,
'timestamp': FieldValue.serverTimestamp(),
});
print('✅ Data berhasil dikirim ke Firestore dengan ID: ${docRef.id}');
} else {
print('⚠️ Tidak ada data dari Realtime Database');
}
} catch (e) {
print('❌ Error auto save dari RTDB ke Firestore: $e');
print('🔍 Detail error: ${e.toString()}');
}
}
@override
void dispose() {
_saveTimer?.cancel();
_countdownTimer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final minutes = (_remainingSeconds ~/ 60).toString().padLeft(2, '0');
final seconds = (_remainingSeconds % 60).toString().padLeft(2, '0');
return Scaffold(
body: Stack(
children: [
if (_pages.isNotEmpty) _pages[_currentIndex],
if (_currentIndex == 0)
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withOpacity(0.1),
borderRadius:
const BorderRadius.vertical(top: Radius.circular(16)),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.timer, color: Colors.blue),
const SizedBox(width: 8),
Text(
'Next save in: $minutes:$seconds',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue,
),
),
],
),
),
),
],
),
bottomNavigationBar: BottomNavigationBar(
currentIndex: _currentIndex,
onTap: (idx) => setState(() => _currentIndex = idx),
items: const [
BottomNavigationBarItem(
icon: Icon(Icons.monitor_heart),
label: 'Monitoring',
),
BottomNavigationBarItem(
icon: Icon(Icons.history),
label: 'Riwayat',
),
],
),
);
}
}
class MonitoringPage extends StatefulWidget {
final Function(double, double, double, double) onDataUpdate;
final Function(Duration) onIntervalChange;
final Duration currentInterval;
const MonitoringPage(
{Key? key,
required this.onDataUpdate,
required this.onIntervalChange,
required this.currentInterval})
: super(key: key);
@override
State<MonitoringPage> createState() => _MonitoringPageState();
}
class _MonitoringPageState extends State<MonitoringPage> {
final DatabaseReference _database = FirebaseDatabase.instance.ref();
double suhu = 30.0;
double kelembaban = 70.0;
double cahaya = 800.0;
double _rainVal = 0.0;
String cuaca = 'Cerah';
bool isLoading = true;
String? error;
// --- Servo & Mode State ---
bool _modeManual = true;
bool _kontrolServo = true;
bool _servo = true;
bool _isUpdatingServo = false;
// --- Firebase Get Function ---
Future<void> firebaseGet() async {
try {
final snapshot = await _database.get();
if (snapshot.value != null) {
final data = snapshot.value as Map<dynamic, dynamic>;
setState(() {
_modeManual = data['modeManual'] ?? true;
_kontrolServo = data['kontrolServo'] ?? true;
_servo = data['servo'] ?? true;
});
}
} catch (e) {
debugPrint('firebaseGet error: $e');
}
}
// --- Update Servo in Firebase ---
Future<void> updateServo(bool value) async {
setState(() => _isUpdatingServo = true);
try {
await _database.child('servo').set(value);
setState(() => _servo = value);
} catch (e) {
debugPrint('updateServo error: $e');
}
setState(() => _isUpdatingServo = false);
}
// --- Update Mode in Firebase ---
Future<void> updateModeManual(bool value) async {
try {
await _database.child('modeManual').set(value);
setState(() => _modeManual = value);
} catch (e) {
debugPrint('updateModeManual error: $e');
}
}
// --- Update KontrolServo in Firebase ---
Future<void> updateKontrolServo(bool value) async {
try {
await _database.child('kontrolServo').set(value);
setState(() => _kontrolServo = value);
} catch (e) {
debugPrint('updateKontrolServo error: $e');
}
}
double _calculateRainPercent(double kadarAir) {
// Menghitung persentase hujan dari nilai kadar air
return ((4095 - (kadarAir / 100 * 4095)) / 4095) * 100;
}
@override
void initState() {
super.initState();
firebaseGet();
_setupRealtimeUpdates();
}
String _updateStatusCuaca(double cahaya) {
// Status cuaca berdasarkan nilai LDR
return (cahaya < 700) ? 'Terang' : 'Gelap';
}
Color _getWeatherStatusColor(String status) {
// Warna untuk setiap status cuaca
switch (status) {
case 'Terang':
return Colors.orange;
case 'Gelap':
return Colors.grey.shade800;
default:
return Colors.grey;
}
}
void _setupRealtimeUpdates() {
_database.onValue.listen((event) async {
if (!mounted) return;
if (event.snapshot.value != null) {
final data = event.snapshot.value as Map<dynamic, dynamic>;
setState(() {
isLoading = false;
suhu = double.parse(data['temperature']?.toString() ?? '30.0');
kelembaban = double.parse(data['humidity']?.toString() ?? '70.0');
cahaya = double.parse(data['ldr']?.toString() ?? '800.0');
_rainVal = double.parse(data['rain']?.toString() ?? '0');
_modeManual = data['modeManual'] ?? true;
_kontrolServo = data['kontrolServo'] ?? true;
_servo = data['servo'] ?? true;
});
// --- Servo Logic ---
if (_modeManual) {
// Manual: servo follows kontrolServo
if (_servo != _kontrolServo) {
await updateServo(_kontrolServo);
}
} else {
// Otomatis: servo reacts to rain
// Konsisten dengan _getStatusHujan: < 500 = Hujan
bool autoServo = _rainVal < 500; // Tertutup jika hujan (< 500)
if (_servo != autoServo) {
await updateServo(autoServo);
}
}
} else {
setState(() {
isLoading = false;
error = 'Tidak ada data';
});
}
}, onError: (e) {
if (mounted) {
setState(() {
error = 'Error: $e';
isLoading = false;
});
}
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Row(
children: [
Text('Monitoring Penjemuran Ikan',
style: TextStyle(
color: Colors.white, fontWeight: FontWeight.bold)),
],
),
backgroundColor: Theme.of(context).colorScheme.primary,
actions: [
PopupMenuButton<Duration>(
icon: const Icon(Icons.timer, color: Colors.white),
initialValue: widget.currentInterval,
onSelected: (value) {
widget.onIntervalChange(value);
},
itemBuilder: (context) => [
const PopupMenuItem(
value: Duration(seconds: 30),
child: Text('30 detik'),
),
const PopupMenuItem(
value: Duration(minutes: 1),
child: Text('1 menit'),
),
const PopupMenuItem(
value: Duration(minutes: 5),
child: Text('5 menit'),
),
const PopupMenuItem(
value: Duration(minutes: 10),
child: Text('10 menit'),
),
const PopupMenuItem(
value: Duration(minutes: 30),
child: Text('30 menit'),
),
const PopupMenuItem(
value: Duration(hours: 1),
child: Text('1 jam'),
),
],
),
],
),
body: isLoading
? const Center(child: CircularProgressIndicator())
: error != null
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline,
color: Colors.red,
size: 60,
),
const SizedBox(height: 16),
Text(
error!,
style: const TextStyle(color: Colors.red),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _setupRealtimeUpdates,
child: const Text('Coba Lagi'),
),
],
),
)
: Container(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Colors.blue.shade50, Colors.white],
),
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// --- Info Cards ---
_buildInfoCard(
'Suhu Udara',
'${suhu.toStringAsFixed(1)}°C',
Colors.red,
Icons.thermostat,
_getStatusSuhu(suhu),
),
const SizedBox(height: 16),
_buildInfoCard(
'Kelembaban',
'${kelembaban.toStringAsFixed(1)}%',
Colors.blue,
Icons.water_drop,
_getStatusKelembaban(kelembaban),
),
const SizedBox(height: 16),
_buildInfoCard(
'Intensitas Cahaya',
'${cahaya.toStringAsFixed(0)}',
Colors.orange,
Icons.wb_sunny,
_getStatusCuaca(cahaya),
),
const SizedBox(height: 16),
_buildInfoCard(
'Curah Hujan',
'${_rainVal.toStringAsFixed(0)}',
Colors.brown,
Icons.water_damage,
_getStatusHujan(_rainVal),
),
const SizedBox(height: 32),
// --- Manual/Otomatis Control (moved to bottom, improved UI) ---
Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
margin: const EdgeInsets.only(top: 8, bottom: 16),
child: Padding(
padding: const EdgeInsets.all(20.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Icons.settings_remote,
color: Theme.of(context)
.colorScheme
.primary),
const SizedBox(width: 10),
const Text(
'Kontrol Servo & Mode',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 18,
),
),
],
),
const SizedBox(height: 16),
Row(
children: [
const Text('Manual',
style: TextStyle(fontSize: 16)),
Switch(
value: _modeManual,
onChanged: (val) async {
await updateModeManual(val);
if (!val) {
// If switching to otomatis, set kontrolServo to false
await updateKontrolServo(false);
}
},
),
const Text('Otomatis',
style: TextStyle(fontSize: 16)),
],
),
if (_modeManual)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
const Text('Servo:',
style: TextStyle(fontSize: 16)),
const SizedBox(width: 8),
_servo
? const Icon(Icons.lock,
color: Colors.blue)
: const Icon(Icons.lock_open,
color: Colors.orange),
const SizedBox(width: 8),
Switch(
value: _kontrolServo,
onChanged: (val) async {
await updateKontrolServo(val);
},
),
_isUpdatingServo
? const SizedBox(
width: 16,
height: 16,
child:
CircularProgressIndicator(
strokeWidth: 2),
)
: const SizedBox.shrink(),
const SizedBox(width: 8),
Text(
_kontrolServo
? 'Tertutup'
: 'Terbuka',
style: TextStyle(
color: _kontrolServo
? Colors.blue
: Colors.orange,
fontWeight: FontWeight.bold,
)),
],
),
),
if (!_modeManual)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
children: [
const Icon(Icons.info_outline,
color: Colors.grey),
const SizedBox(width: 8),
Text(
'Servo otomatis berdasarkan curah hujan',
style: TextStyle(
color: Colors.grey.shade700,
fontSize: 10),
),
],
),
),
],
),
),
),
const SizedBox(height: 80), // Space for countdown
],
),
),
),
);
}
Widget _buildInfoCard(
String title, String value, Color color, IconData icon, String status) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
children: [
Icon(
icon,
size: 40,
color: color,
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Row(
children: [
Text(
title == 'Intensitas Cahaya'
? '${cahaya.toStringAsFixed(0)}'
: value,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color,
),
),
],
),
],
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: title == 'Suhu Udara'
? _getStatusSuhuColor(status).withOpacity(0.2)
: _getStatusColor(status).withOpacity(0.2),
borderRadius: BorderRadius.circular(12),
),
child: Text(
status,
style: TextStyle(
color: title == 'Status Cuaca'
? _getWeatherStatusColor(status)
: title == 'Suhu Udara'
? _getStatusSuhuColor(status)
: _getStatusColor(status),
fontWeight: FontWeight.bold,
),
),
),
],
),
),
);
}
String _getStatusSuhu(double suhu) {
// Status suhu berdasarkan nilai sensor
if (suhu <= 20) return 'Dingin';
if (suhu <= 23) return 'Sejuk';
if (suhu <= 26) return 'Normal';
if (suhu <= 27) return 'Hangat';
return 'Panas';
}
String _getStatusKelembaban(double kelembaban) {
// Status kelembaban berdasarkan nilai sensor
if (kelembaban < 40) return 'Kering';
if (kelembaban > 80) return 'Basah';
return 'Normal';
}
String _getStatusHujan(double kadar) {
// Status hujan berdasarkan nilai sensor
return (kadar < 500) ? "Hujan" : "Tidak Hujan";
}
String _getStatusCuaca(double cahaya) {
// Status cahaya berdasarkan nilai LDR
return (cahaya < 700) ? "Terang" : "Gelap";
}
Color _getStatusColor(String status) {
// Warna untuk setiap status sensor
switch (status) {
case 'Hujan':
return Colors.blue.shade700;
case 'Tidak Hujan':
return Colors.orange;
case 'Cerah':
return Colors.orange;
default:
return Colors.grey;
}
}
Color _getStatusSuhuColor(String status) {
// Warna untuk setiap status suhu
switch (status) {
case 'Dingin':
return Colors.blue.shade700;
case 'Sejuk':
return Colors.blue.shade400;
case 'Normal':
return Colors.orange;
case 'Hangat':
return Colors.orange.shade700;
case 'Panas':
return Colors.red;
default:
return Colors.grey;
}
}
}

591
pubspec.lock Normal file
View File

@ -0,0 +1,591 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7"
url: "https://pub.dev"
source: hosted
version: "1.3.35"
android_alarm_manager_plus:
dependency: "direct main"
description:
name: android_alarm_manager_plus
sha256: "3aaf2bd35177e60a7e90accd9d615d35408b556339becec22cc1284babbba8b7"
url: "https://pub.dev"
source: hosted
version: "4.0.7"
args:
dependency: transitive
description:
name: args
sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
url: "https://pub.dev"
source: hosted
version: "2.7.0"
async:
dependency: transitive
description:
name: async
sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c"
url: "https://pub.dev"
source: hosted
version: "2.11.0"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
characters:
dependency: transitive
description:
name: characters
sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605"
url: "https://pub.dev"
source: hosted
version: "1.3.0"
clock:
dependency: transitive
description:
name: clock
sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf
url: "https://pub.dev"
source: hosted
version: "1.1.1"
cloud_firestore:
dependency: "direct main"
description:
name: cloud_firestore
sha256: a0f161b92610e078b4962d7e6ebeb66dc9cce0ada3514aeee442f68165d78185
url: "https://pub.dev"
source: hosted
version: "4.17.5"
cloud_firestore_platform_interface:
dependency: transitive
description:
name: cloud_firestore_platform_interface
sha256: "6a55b319f8d33c307396b9104512e8130a61904528ab7bd8b5402678fca54b81"
url: "https://pub.dev"
source: hosted
version: "6.2.5"
cloud_firestore_web:
dependency: transitive
description:
name: cloud_firestore_web
sha256: "89dfa1304d3da48b3039abbb2865e3d30896ef858e569a16804a99f4362283a9"
url: "https://pub.dev"
source: hosted
version: "3.12.5"
collection:
dependency: transitive
description:
name: collection
sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a
url: "https://pub.dev"
source: hosted
version: "1.18.0"
dbus:
dependency: transitive
description:
name: dbus
sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c"
url: "https://pub.dev"
source: hosted
version: "0.7.11"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78"
url: "https://pub.dev"
source: hosted
version: "1.3.1"
ffi:
dependency: transitive
description:
name: ffi
sha256: "16ed7b077ef01ad6170a3d0c57caa4a112a38d7a2ed5602e0aca9ca6f3d98da6"
url: "https://pub.dev"
source: hosted
version: "2.1.3"
file:
dependency: transitive
description:
name: file
sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d"
url: "https://pub.dev"
source: hosted
version: "6.1.4"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c"
url: "https://pub.dev"
source: hosted
version: "2.32.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf
url: "https://pub.dev"
source: hosted
version: "5.4.0"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: faa5a76f6380a9b90b53bc3bdcb85bc7926a382e0709b9b5edac9f7746651493
url: "https://pub.dev"
source: hosted
version: "2.21.1"
firebase_database:
dependency: "direct main"
description:
name: firebase_database
sha256: "3b9ca306d26ad243ccbc4c717ff6e8563a080ebe11ee77fa7349b419c894b42d"
url: "https://pub.dev"
source: hosted
version: "10.5.7"
firebase_database_platform_interface:
dependency: transitive
description:
name: firebase_database_platform_interface
sha256: "5864cc362275465e9bd682b243f19419c9d78b861c2db820241eea596ae3b320"
url: "https://pub.dev"
source: hosted
version: "0.2.5+35"
firebase_database_web:
dependency: transitive
description:
name: firebase_database_web
sha256: a6008395dd20e8b8dde0691b441c181a1216c3866f89f48dcb6889d34fd35905
url: "https://pub.dev"
source: hosted
version: "0.2.5+7"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_background_service:
dependency: "direct main"
description:
name: flutter_background_service
sha256: "70a1c185b1fa1a44f8f14ecd6c86f6e50366e3562f00b2fa5a54df39b3324d3d"
url: "https://pub.dev"
source: hosted
version: "5.1.0"
flutter_background_service_android:
dependency: transitive
description:
name: flutter_background_service_android
sha256: b73d903056240e23a5c56d9e52d3a5d02ae41cb18b2988a97304ae37b2bae4bf
url: "https://pub.dev"
source: hosted
version: "6.3.0"
flutter_background_service_ios:
dependency: transitive
description:
name: flutter_background_service_ios
sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba"
url: "https://pub.dev"
source: hosted
version: "5.0.3"
flutter_background_service_platform_interface:
dependency: transitive
description:
name: flutter_background_service_platform_interface
sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41
url: "https://pub.dev"
source: hosted
version: "5.1.2"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
url: "https://pub.dev"
source: hosted
version: "2.0.3"
flutter_local_notifications:
dependency: "direct main"
description:
name: flutter_local_notifications
sha256: ef41ae901e7529e52934feba19ed82827b11baa67336829564aeab3129460610
url: "https://pub.dev"
source: hosted
version: "18.0.1"
flutter_local_notifications_linux:
dependency: transitive
description:
name: flutter_local_notifications_linux
sha256: "8f685642876742c941b29c32030f6f4f6dacd0e4eaecb3efbb187d6a3812ca01"
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_local_notifications_platform_interface:
dependency: transitive
description:
name: flutter_local_notifications_platform_interface
sha256: "6c5b83c86bf819cdb177a9247a3722067dd8cc6313827ce7c77a4b238a26fd52"
url: "https://pub.dev"
source: hosted
version: "8.0.0"
flutter_localizations:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
http:
dependency: transitive
description:
name: http
sha256: "2c11f3f94c687ee9bad77c171151672986360b2b001d109814ee7140b2cf261b"
url: "https://pub.dev"
source: hosted
version: "1.4.0"
http_parser:
dependency: transitive
description:
name: http_parser
sha256: "2aa08ce0341cc9b354a498388e30986515406668dbcc4f7c950c3e715496693b"
url: "https://pub.dev"
source: hosted
version: "4.0.2"
intl:
dependency: "direct main"
description:
name: intl
sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf
url: "https://pub.dev"
source: hosted
version: "0.19.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05"
url: "https://pub.dev"
source: hosted
version: "10.0.5"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806"
url: "https://pub.dev"
source: hosted
version: "3.0.5"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
url: "https://pub.dev"
source: hosted
version: "3.0.1"
lints:
dependency: transitive
description:
name: lints
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
url: "https://pub.dev"
source: hosted
version: "2.1.1"
matcher:
dependency: transitive
description:
name: matcher
sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb
url: "https://pub.dev"
source: hosted
version: "0.12.16+1"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7
url: "https://pub.dev"
source: hosted
version: "1.15.0"
path:
dependency: transitive
description:
name: path
sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af"
url: "https://pub.dev"
source: hosted
version: "1.9.0"
path_provider_linux:
dependency: transitive
description:
name: path_provider_linux
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
url: "https://pub.dev"
source: hosted
version: "2.2.1"
path_provider_platform_interface:
dependency: transitive
description:
name: path_provider_platform_interface
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
path_provider_windows:
dependency: transitive
description:
name: path_provider_windows
sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7
url: "https://pub.dev"
source: hosted
version: "2.3.0"
petitparser:
dependency: transitive
description:
name: petitparser
sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27
url: "https://pub.dev"
source: hosted
version: "6.0.2"
platform:
dependency: transitive
description:
name: platform
sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984"
url: "https://pub.dev"
source: hosted
version: "3.1.6"
plugin_platform_interface:
dependency: transitive
description:
name: plugin_platform_interface
sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02"
url: "https://pub.dev"
source: hosted
version: "2.1.8"
process:
dependency: transitive
description:
name: process
sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09"
url: "https://pub.dev"
source: hosted
version: "4.2.4"
shared_preferences:
dependency: "direct main"
description:
name: shared_preferences
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
url: "https://pub.dev"
source: hosted
version: "2.5.3"
shared_preferences_android:
dependency: transitive
description:
name: shared_preferences_android
sha256: "9f9f3d372d4304723e6136663bb291c0b93f5e4c8a4a6314347f481a33bda2b1"
url: "https://pub.dev"
source: hosted
version: "2.4.7"
shared_preferences_foundation:
dependency: transitive
description:
name: shared_preferences_foundation
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
url: "https://pub.dev"
source: hosted
version: "2.5.4"
shared_preferences_linux:
dependency: transitive
description:
name: shared_preferences_linux
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_platform_interface:
dependency: transitive
description:
name: shared_preferences_platform_interface
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
shared_preferences_web:
dependency: transitive
description:
name: shared_preferences_web
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
url: "https://pub.dev"
source: hosted
version: "2.4.3"
shared_preferences_windows:
dependency: transitive
description:
name: shared_preferences_windows
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
url: "https://pub.dev"
source: hosted
version: "2.4.1"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.99"
source_span:
dependency: transitive
description:
name: source_span
sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c"
url: "https://pub.dev"
source: hosted
version: "1.10.0"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b"
url: "https://pub.dev"
source: hosted
version: "1.11.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7
url: "https://pub.dev"
source: hosted
version: "2.1.2"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde"
url: "https://pub.dev"
source: hosted
version: "1.2.0"
syncfusion_flutter_charts:
dependency: "direct main"
description:
name: syncfusion_flutter_charts
sha256: ab73109c586f5ec2b01adc2672026a1fb3f93b2b5f6061ba8d7126c119061002
url: "https://pub.dev"
source: hosted
version: "24.2.9"
syncfusion_flutter_core:
dependency: transitive
description:
name: syncfusion_flutter_core
sha256: "7666506885ebc8f62bb928ad4588a73e20caaff2b2cf2b2b56f67d98f4113525"
url: "https://pub.dev"
source: hosted
version: "24.2.9"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84
url: "https://pub.dev"
source: hosted
version: "1.2.1"
test_api:
dependency: transitive
description:
name: test_api
sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb"
url: "https://pub.dev"
source: hosted
version: "0.7.2"
timezone:
dependency: transitive
description:
name: timezone
sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1
url: "https://pub.dev"
source: hosted
version: "0.10.1"
typed_data:
dependency: transitive
description:
name: typed_data
sha256: f9049c039ebfeb4cf7a7104a675823cd72dba8297f264b6637062516699fa006
url: "https://pub.dev"
source: hosted
version: "1.4.0"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d"
url: "https://pub.dev"
source: hosted
version: "14.2.5"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
workmanager:
dependency: "direct main"
description:
name: workmanager
sha256: ed13530cccd28c5c9959ad42d657cd0666274ca74c56dea0ca183ddd527d3a00
url: "https://pub.dev"
source: hosted
version: "0.5.2"
xdg_directories:
dependency: transitive
description:
name: xdg_directories
sha256: bd512f03919aac5f1313eb8249f223bacf4927031bf60b02601f81f687689e86
url: "https://pub.dev"
source: hosted
version: "0.2.0+3"
xml:
dependency: transitive
description:
name: xml
sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226
url: "https://pub.dev"
source: hosted
version: "6.5.0"
sdks:
dart: ">=3.5.0 <4.0.0"
flutter: ">=3.24.0"

100
pubspec.yaml Normal file
View File

@ -0,0 +1,100 @@
name: flutter_monitoringhujan
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
version: 1.0.0+1
environment:
sdk: '>=3.2.3 <4.0.0'
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
dependencies:
flutter:
sdk: flutter
flutter_localizations:
sdk: flutter
firebase_core: ^2.27.1
firebase_database: ^10.4.7
cloud_firestore: ^4.15.5
intl: ^0.19.0
shared_preferences: ^2.2.2
flutter_background_service: ^5.0.5
workmanager: ^0.5.2
android_alarm_manager_plus: ^4.0.0
syncfusion_flutter_charts: ^24.1.41
flutter_local_notifications: ^18.0.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
dev_dependencies:
flutter_test:
sdk: flutter
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
flutter_lints: ^2.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, add a fonts section here,
# in this "flutter" section. Each entry in the list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts from package dependencies,
# see https://flutter.dev/to/font-from-package

30
test/widget_test.dart Normal file
View File

@ -0,0 +1,30 @@
// This is a basic Flutter widget test.
//
// To perform an interaction with a widget in your test, use the WidgetTester
// utility in the flutter_test package. For example, you can send tap and scroll
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:flutter_monitoringhujan/main.dart';
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);
expect(find.text('1'), findsNothing);
// Tap the '+' icon and trigger a frame.
await tester.tap(find.byIcon(Icons.add));
await tester.pump();
// Verify that our counter has incremented.
expect(find.text('0'), findsNothing);
expect(find.text('1'), findsOneWidget);
});
}

BIN
web/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 917 B

BIN
web/icons/Icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

BIN
web/icons/Icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

38
web/index.html Normal file
View File

@ -0,0 +1,38 @@
<!DOCTYPE html>
<html>
<head>
<!--
If you are serving your web app in a path other than the root, change the
href value below to reflect the base path you are serving from.
The path provided below has to start and end with a slash "/" in order for
it to work correctly.
For more details:
* https://developer.mozilla.org/en-US/docs/Web/HTML/Element/base
This is a placeholder for base href that will be replaced by the value of
the `--base-href` argument provided to `flutter build`.
-->
<base href="$FLUTTER_BASE_HREF">
<meta charset="UTF-8">
<meta content="IE=Edge" http-equiv="X-UA-Compatible">
<meta name="description" content="A new Flutter project.">
<!-- iOS meta tags & icons -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<meta name="apple-mobile-web-app-title" content="flutter_monitoringhujan">
<link rel="apple-touch-icon" href="icons/Icon-192.png">
<!-- Favicon -->
<link rel="icon" type="image/png" href="favicon.png"/>
<title>flutter_monitoringhujan</title>
<link rel="manifest" href="manifest.json">
</head>
<body>
<script src="flutter_bootstrap.js" async></script>
</body>
</html>

35
web/manifest.json Normal file
View File

@ -0,0 +1,35 @@
{
"name": "flutter_monitoringhujan",
"short_name": "flutter_monitoringhujan",
"start_url": ".",
"display": "standalone",
"background_color": "#0175C2",
"theme_color": "#0175C2",
"description": "A new Flutter project.",
"orientation": "portrait-primary",
"prefer_related_applications": false,
"icons": [
{
"src": "icons/Icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/Icon-512.png",
"sizes": "512x512",
"type": "image/png"
},
{
"src": "icons/Icon-maskable-192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "icons/Icon-maskable-512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
]
}