Pengusir-Hama-Burung-Otomatis/project/tugas_akhir/lib/screens/beranda_page.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
}