531 lines
20 KiB
Dart
531 lines
20 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:firebase_database/firebase_database.dart';
|
|
import 'package:firebase_core/firebase_core.dart';
|
|
import 'package:fl_chart/fl_chart.dart';
|
|
import '../services/notif_service.dart';
|
|
import 'notifikasi_page.dart';
|
|
import 'package:intl/intl.dart';
|
|
|
|
final db = FirebaseDatabase.instanceFor(
|
|
app: Firebase.app(),
|
|
databaseURL:
|
|
"https://tugas-akhir-9947b-default-rtdb.asia-southeast1.firebasedatabase.app",
|
|
).ref();
|
|
|
|
class BerandaPage extends StatefulWidget {
|
|
const BerandaPage({super.key});
|
|
|
|
@override
|
|
State<BerandaPage> createState() => _BerandaPageState();
|
|
}
|
|
|
|
class _BerandaPageState extends State<BerandaPage> {
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
loadRiwayat();
|
|
}
|
|
|
|
// =====================================================
|
|
// DATA GRAFIK
|
|
// =====================================================
|
|
final List<FlSpot> burungData = [];
|
|
final List<FlSpot> manusiaData = [];
|
|
int index = 1;
|
|
|
|
// =====================================================
|
|
// LAST REALTIME DATA
|
|
// =====================================================
|
|
String lastJarak = "-- cm";
|
|
String lastSuara = "35";
|
|
String lastDeteksiRealtime = "--";
|
|
String pir1Status = "0";
|
|
String pir2Status = "0";
|
|
|
|
// =====================================================
|
|
// LAST VALUE GRAFIK
|
|
// =====================================================
|
|
double lastBurungValue = 0;
|
|
double lastManusiaValue = 0;
|
|
|
|
// =====================================================
|
|
// NOTIF TIMER
|
|
// =====================================================
|
|
DateTime? lastNotifTime;
|
|
bool burungSudahNotif = false;
|
|
|
|
bool historiLoaded = false;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (!historiLoaded) {
|
|
return const Scaffold(body: Center(child: CircularProgressIndicator()));
|
|
}
|
|
return Scaffold(
|
|
backgroundColor: const Color(0xFFF2F2F2),
|
|
appBar: AppBar(
|
|
title: const Text(
|
|
"Beranda",
|
|
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
|
|
),
|
|
backgroundColor: const Color(0xFF6FCF97),
|
|
elevation: 0,
|
|
actions: [
|
|
IconButton(
|
|
icon: const Icon(Icons.notifications, color: Colors.white),
|
|
onPressed: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(builder: (context) => const NotifikasiPage()),
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
body: SingleChildScrollView(
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16),
|
|
child: Column(
|
|
children: [
|
|
// =====================================================
|
|
// STATUS SISTEM
|
|
// =====================================================
|
|
StreamBuilder<DatabaseEvent>(
|
|
stream: db.child("Kebun/Status").onValue,
|
|
builder: (context, snapshot) {
|
|
String status = "Menghubungkan...";
|
|
|
|
if (snapshot.hasData &&
|
|
snapshot.data!.snapshot.value != null) {
|
|
status = snapshot.data!.snapshot.value.toString();
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Status Sistem",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFE8F5E9),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.all(20),
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFF4CAF50),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.shield,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Text(
|
|
status,
|
|
style: const TextStyle(
|
|
fontSize: 20,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// =====================================================
|
|
// SENSOR REALTIME
|
|
// =====================================================
|
|
StreamBuilder<DatabaseEvent>(
|
|
stream: db.child("Kebun/Sensor").onValue,
|
|
builder: (context, snapshot) {
|
|
double dbValue = 35;
|
|
|
|
if (snapshot.hasData &&
|
|
snapshot.data!.snapshot.value != null) {
|
|
final data = Map<String, dynamic>.from(
|
|
snapshot.data!.snapshot.value as Map,
|
|
);
|
|
|
|
pir1Status = data['PIR1']?.toString() ?? "0";
|
|
pir2Status = data['PIR2']?.toString() ?? "0";
|
|
|
|
// =========================================
|
|
// SUARA & SENSOR REALTIME
|
|
// =========================================
|
|
double suaraBaru =
|
|
double.tryParse(data['Suara_dB'].toString()) ??
|
|
double.tryParse(lastSuara) ??
|
|
35;
|
|
|
|
if (suaraBaru <= 0) {
|
|
suaraBaru = double.tryParse(lastSuara) ?? 35;
|
|
}
|
|
|
|
dbValue = suaraBaru;
|
|
lastSuara = dbValue.toStringAsFixed(1);
|
|
|
|
// =========================================
|
|
// DETEKSI LOGIKA TAMPILAN CLEAN
|
|
// =========================================
|
|
String realtimeDeteksi =
|
|
data['Deteksi']?.toString().toLowerCase() ??
|
|
"tidak dikenal";
|
|
|
|
if (realtimeDeteksi.contains("burung") ||
|
|
realtimeDeteksi.contains("manusia")) {
|
|
if (data['Jarak'] != null) {
|
|
lastJarak = "${data['Jarak']} cm";
|
|
}
|
|
}
|
|
|
|
if (realtimeDeteksi.contains("burung")) {
|
|
lastDeteksiRealtime = "burung";
|
|
} else if (realtimeDeteksi.contains("manusia")) {
|
|
lastDeteksiRealtime = "manusia";
|
|
} else {
|
|
lastDeteksiRealtime = data['Deteksi']?.toString() ?? "--";
|
|
}
|
|
|
|
// =========================================
|
|
// PERBAIKAN LOGIKA GRAFIK & NOTIFIKASI
|
|
// =========================================
|
|
if (realtimeDeteksi == "burung" &&
|
|
(pir1Status == "1" || pir2Status == "1")) {
|
|
if (!burungSudahNotif) {
|
|
burungSudahNotif = true;
|
|
String waktu = DateFormat(
|
|
'dd-MM-yyyy HH:mm:ss',
|
|
).format(DateTime.now());
|
|
String jarakNotif = data['Jarak']?.toString() ?? "-";
|
|
String suaraNotif = data['Suara_dB']?.toString() ?? "-";
|
|
|
|
NotifService.showNotif(
|
|
"Burung Terdeteksi",
|
|
"Gerakan terdeteksi\nJarak: $jarakNotif cm\nSuara: $suaraNotif dB\nWaktu: $waktu",
|
|
);
|
|
}
|
|
|
|
// Hanya tambah titik baru jika nilai dbValue berubah dari titik terakhir
|
|
if (burungData.isEmpty || burungData.last.y != dbValue) {
|
|
lastBurungValue = dbValue;
|
|
lastManusiaValue = 0;
|
|
|
|
burungData.add(FlSpot(index.toDouble(), dbValue));
|
|
manusiaData.add(FlSpot(index.toDouble(), 0));
|
|
index++;
|
|
}
|
|
} else if (realtimeDeteksi.contains("manusia")) {
|
|
burungSudahNotif = false;
|
|
|
|
// Hanya tambah titik baru jika nilai dbValue berubah dari titik terakhir
|
|
if (manusiaData.isEmpty ||
|
|
manusiaData.last.y != dbValue) {
|
|
lastManusiaValue = dbValue;
|
|
lastBurungValue = 0;
|
|
|
|
manusiaData.add(FlSpot(index.toDouble(), dbValue));
|
|
burungData.add(FlSpot(index.toDouble(), 0));
|
|
index++;
|
|
}
|
|
} else {
|
|
burungSudahNotif = false;
|
|
// JIKA TIDAK ADA DETEKSI BARU, JANGAN TAMBAHKAN TITIK (Grafik Diam/Berhenti)
|
|
}
|
|
|
|
// =========================================
|
|
// LIMIT DATA GRAFIK
|
|
// =========================================
|
|
if (burungData.length > 50) {
|
|
burungData.removeAt(0);
|
|
}
|
|
if (manusiaData.length > 50) {
|
|
manusiaData.removeAt(0);
|
|
}
|
|
}
|
|
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
const Text(
|
|
"Status Sensor",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
fontSize: 16,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
|
|
// CARD SENSOR
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF6FCF97),
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
children: [
|
|
_buildItem("Jarak", lastJarak),
|
|
_buildItem("Suara", lastSuara),
|
|
_buildItem("Deteksi", lastDeteksiRealtime),
|
|
],
|
|
),
|
|
const SizedBox(height: 15),
|
|
const Divider(),
|
|
const SizedBox(height: 10),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: _buildPirCard("PIR 1", pir1Status),
|
|
),
|
|
const SizedBox(width: 10),
|
|
Expanded(
|
|
child: _buildPirCard("PIR 2", pir2Status),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 20),
|
|
|
|
// GRAFIK CONTAINER
|
|
Container(
|
|
padding: const EdgeInsets.all(16),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
// LEGENDA
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 15,
|
|
height: 15,
|
|
color: Colors.red,
|
|
),
|
|
const SizedBox(width: 5),
|
|
const Text(
|
|
"Manusia",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(width: 20),
|
|
Row(
|
|
children: [
|
|
Container(
|
|
width: 15,
|
|
height: 15,
|
|
color: Colors.blue,
|
|
),
|
|
const SizedBox(width: 5),
|
|
const Text(
|
|
"Burung",
|
|
style: TextStyle(
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 15),
|
|
SizedBox(
|
|
height: 250,
|
|
child: LineChart(
|
|
LineChartData(
|
|
// PERBAIKAN: Agar garis dipotong rapi dan tidak bocor keluar kotak y atau x
|
|
clipData: const FlClipData.all(),
|
|
minX: index > 20
|
|
? (index - 20).toDouble()
|
|
: 0,
|
|
maxX: index.toDouble(),
|
|
minY: 0,
|
|
maxY: 80,
|
|
borderData: FlBorderData(show: false),
|
|
gridData: FlGridData(show: true),
|
|
titlesData: FlTitlesData(
|
|
bottomTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
rightTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
topTitles: const AxisTitles(
|
|
sideTitles: SideTitles(showTitles: false),
|
|
),
|
|
leftTitles: const AxisTitles(
|
|
sideTitles: SideTitles(
|
|
showTitles: true,
|
|
reservedSize: 35,
|
|
),
|
|
),
|
|
),
|
|
lineBarsData: [
|
|
// LINE BURUNG
|
|
LineChartBarData(
|
|
spots: burungData,
|
|
color: Colors.blue,
|
|
isCurved: true,
|
|
curveSmoothness: 0.3,
|
|
preventCurveOverShooting: true,
|
|
barWidth: 4,
|
|
dotData: const FlDotData(show: false),
|
|
),
|
|
// LINE MANUSIA
|
|
LineChartBarData(
|
|
spots: manusiaData,
|
|
color: Colors.red,
|
|
isCurved: true,
|
|
preventCurveOverShooting: true,
|
|
barWidth: 4,
|
|
dotData: const FlDotData(show: false),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> loadRiwayat() async {
|
|
final snapshot = await db.child("Kebun/Riwayat").limitToLast(50).get();
|
|
|
|
if (!snapshot.exists) {
|
|
historiLoaded = true;
|
|
setState(() {});
|
|
return;
|
|
}
|
|
|
|
final data = Map<String, dynamic>.from(snapshot.value as Map);
|
|
|
|
burungData.clear();
|
|
manusiaData.clear();
|
|
|
|
int i = 0;
|
|
|
|
for (var item in data.values) {
|
|
final row = Map<String, dynamic>.from(item);
|
|
print("DATA RIWAYAT: $row");
|
|
|
|
double suara = double.tryParse(row['db'].toString()) ?? 0;
|
|
|
|
String deteksi = row['deteksi'].toString().toLowerCase();
|
|
|
|
print("Deteksi: $deteksi | db: $suara");
|
|
|
|
if (deteksi.contains("burung")) {
|
|
burungData.add(FlSpot(i.toDouble(), suara));
|
|
|
|
manusiaData.add(FlSpot(i.toDouble(), 0));
|
|
}
|
|
|
|
if (deteksi.contains("manusia")) {
|
|
manusiaData.add(FlSpot(i.toDouble(), suara));
|
|
|
|
burungData.add(FlSpot(i.toDouble(), 0));
|
|
}
|
|
|
|
lastSuara = suara.toStringAsFixed(1);
|
|
lastDeteksiRealtime = deteksi;
|
|
lastJarak = "${row['jarak']} cm";
|
|
|
|
i++;
|
|
}
|
|
|
|
index = i;
|
|
|
|
historiLoaded = true;
|
|
|
|
setState(() {});
|
|
}
|
|
|
|
Widget _buildItem(String title, String value) {
|
|
return Column(
|
|
children: [
|
|
Text(title, style: const TextStyle(fontSize: 14)),
|
|
const SizedBox(height: 5),
|
|
Text(
|
|
value,
|
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
Widget _buildPirCard(String title, String status) {
|
|
bool aktif = status == "1";
|
|
|
|
return Container(
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: aktif ? Colors.red.shade50 : Colors.green.shade50,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: aktif ? Colors.red : Colors.green, width: 2),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Text(
|
|
title,
|
|
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
|
|
),
|
|
const SizedBox(height: 10),
|
|
Icon(
|
|
Icons.directions_run,
|
|
size: 35,
|
|
color: aktif ? Colors.red : Colors.green,
|
|
),
|
|
const SizedBox(height: 8),
|
|
Text(
|
|
aktif ? "Terdeteksi" : "Tidak Ada",
|
|
style: TextStyle(
|
|
color: aktif ? Colors.red : Colors.green,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
); // <--- PASTIKAN BARIS INI ADA DI BARIS 463 Anda
|
|
}
|