515 lines
15 KiB
Dart
515 lines
15 KiB
Dart
import 'package:cupang_app/models/FirebaseData.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'dart:async';
|
|
import 'package:intl/intl.dart'; // Import package intl
|
|
import 'package:toast/toast.dart';
|
|
import 'mqtt_service.dart'; // Import MQTT service
|
|
import 'package:flutter/services.dart'; // Import for ToastContext
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
import 'dart:convert'; // For json encoding/decoding
|
|
|
|
void main() {
|
|
runApp(const MyApp());
|
|
}
|
|
|
|
class MyApp extends StatelessWidget {
|
|
const MyApp({Key? key});
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MaterialApp(
|
|
title: 'Flutter Demo',
|
|
theme: ThemeData(
|
|
colorScheme: const ColorScheme.light(background: Colors.white),
|
|
useMaterial3: true,
|
|
),
|
|
home: const MyHomePage(title: 'Flutter Demo Home Page'),
|
|
debugShowCheckedModeBanner: false,
|
|
);
|
|
}
|
|
}
|
|
|
|
class MyHomePage extends StatefulWidget {
|
|
const MyHomePage({Key? key, required this.title}) : super(key: key);
|
|
|
|
final String title;
|
|
|
|
@override
|
|
State<MyHomePage> createState() => _MyHomePageState();
|
|
}
|
|
|
|
class _MyHomePageState extends State<MyHomePage> {
|
|
String? pagi;
|
|
String? siang;
|
|
String? malam;
|
|
int pagiPutaran = 0;
|
|
int siangPutaran = 0;
|
|
int malamPutaran = 0;
|
|
TimeOfDay _timeOfDay = TimeOfDay.now();
|
|
late MQTTService mqttService;
|
|
List<Map<String, dynamic>> feedingHistory = [];
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_loadFeedingHistory();
|
|
mqttService = MQTTService();
|
|
mqttService.initializeMQTT();
|
|
reload(); // Load data when widget is first initialized
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
ToastContext().init(context);
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
backgroundColor: Colors.blue.shade900,
|
|
title: const Text(
|
|
'PiCo',
|
|
style: TextStyle(color: Colors.white),
|
|
),
|
|
centerTitle: true,
|
|
actions: [
|
|
IconButton(
|
|
icon: Icon(Icons.history),
|
|
onPressed: _showFeedingHistory,
|
|
),
|
|
],
|
|
),
|
|
body: Column(
|
|
children: [
|
|
const SizedBox(height: 10),
|
|
buildScheduleCard('Pagi', pagi, pagiPutaran, _setPagi),
|
|
buildScheduleCard('Siang', siang, siangPutaran, _setSiang),
|
|
buildScheduleCard('Malam', malam, malamPutaran, _setMalam),
|
|
],
|
|
),
|
|
floatingActionButton: Padding(
|
|
padding: const EdgeInsets.only(bottom: 30.0),
|
|
child: FloatingActionButton.extended(
|
|
onPressed: _manualFeed,
|
|
label: const Text('Pakan Manual'),
|
|
backgroundColor: Colors.blue, // Change background color as needed
|
|
),
|
|
),
|
|
floatingActionButtonLocation: FloatingActionButtonLocation.centerDocked,
|
|
);
|
|
}
|
|
|
|
Widget buildScheduleCard(
|
|
String title, String? time, int putaran, Function setFunction) {
|
|
return Container(
|
|
height: 100,
|
|
margin: const EdgeInsets.all(10),
|
|
decoration: const BoxDecoration(
|
|
color: Colors.blue,
|
|
borderRadius: BorderRadius.all(Radius.circular(10)),
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 20),
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
if (time != null)
|
|
Text(
|
|
time,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 27,
|
|
),
|
|
)
|
|
else
|
|
const CircularProgressIndicator(color: Colors.white),
|
|
Text(
|
|
title,
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
Text(
|
|
'Putaran: $putaran',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 14,
|
|
),
|
|
),
|
|
],
|
|
)),
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 20),
|
|
child: ElevatedButton(
|
|
onPressed: () => setFunction(),
|
|
child: const Text('Set', style: TextStyle(color: Colors.black)),
|
|
style: ElevatedButton.styleFrom(backgroundColor: Colors.white),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _loadFeedingHistory() async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
final String? historyString = prefs.getString('feedingHistory');
|
|
if (historyString != null) {
|
|
setState(() {
|
|
feedingHistory =
|
|
List<Map<String, dynamic>>.from(json.decode(historyString));
|
|
});
|
|
}
|
|
}
|
|
|
|
Future<void> _saveFeedingHistory(Map<String, dynamic> record) async {
|
|
final prefs = await SharedPreferences.getInstance();
|
|
|
|
// Format tanggal dan waktu
|
|
final formattedDate = DateFormat('yyyy-MM-dd').format(DateTime.now());
|
|
record['tanggal'] =
|
|
formattedDate; // Tambahkan tanggal dengan format yang diinginkan
|
|
|
|
// Tambahkan riwayat baru di awal list
|
|
feedingHistory.insert(0, record);
|
|
if (feedingHistory.length > 10) {
|
|
feedingHistory.removeLast(); // Hapus item paling lama jika melebihi 10
|
|
}
|
|
await prefs.setString('feedingHistory', json.encode(feedingHistory));
|
|
}
|
|
|
|
Future<void> _showFeedingHistory() async {
|
|
await _loadFeedingHistory();
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text('Riwayat Pemberian Pakan'),
|
|
content: SizedBox(
|
|
width: double.maxFinite,
|
|
child: ListView.builder(
|
|
shrinkWrap: true,
|
|
itemCount: feedingHistory.length,
|
|
itemBuilder: (BuildContext context, int index) {
|
|
final record = feedingHistory[index];
|
|
return ListTile(
|
|
title: Text(record['time']),
|
|
subtitle: Text(
|
|
'Putaran: ${record['putaran']}, Tanggal: ${record['tanggal']}'),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
actions: <Widget>[
|
|
TextButton(
|
|
child: Text('OK'),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Future<void> _setPagi() async {
|
|
TimeOfDay? picked = await showTimePicker(
|
|
context: context,
|
|
initialTime: _timeOfDay,
|
|
);
|
|
|
|
if (picked != null) {
|
|
// Validasi waktu yang dipilih
|
|
if (!isTimeInPagiRange(picked)) {
|
|
// Gunakan Builder widget untuk mendapatkan BuildContext
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text('Pilih waktu antara 03:00 hingga 10.59 untuk pagi'),
|
|
actions: <Widget>[
|
|
TextButton(
|
|
child: Text('OK'),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
|
|
String jam =
|
|
'${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}';
|
|
int putaran = await _showPutaranDialog();
|
|
showLoadingDialog(context);
|
|
// Update data di Firebase
|
|
await FirebaseData(pagi: jam, pagiPutaran: putaran).toJson();
|
|
Navigator.of(context).pop();
|
|
|
|
// Tampilkan toast ketika waktu pakan otomatis telah tiba
|
|
scheduleNotification(picked,
|
|
const Duration(seconds: 5)); // Notifikasi 5 detik lebih lambat
|
|
|
|
reload();
|
|
}
|
|
}
|
|
|
|
bool isTimeInPagiRange(TimeOfDay time) {
|
|
// Rentang waktu pagi adalah dari jam 03:00 hingga 10:59
|
|
return time.hour >= 3 && time.hour < 11;
|
|
}
|
|
|
|
Future<void> _setSiang() async {
|
|
TimeOfDay? picked = await showTimePicker(
|
|
context: context,
|
|
initialTime: _timeOfDay,
|
|
);
|
|
|
|
if (picked != null) {
|
|
// Validasi waktu yang dipilih
|
|
if (!isTimeInSiangRange(picked)) {
|
|
// Gunakan Builder widget untuk mendapatkan BuildContext
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text('Pilih waktu antara 11:00 hingga 15:59 untuk siang'),
|
|
actions: <Widget>[
|
|
TextButton(
|
|
child: Text('OK'),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
|
|
String jam =
|
|
'${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}';
|
|
int putaran = await _showPutaranDialog();
|
|
showLoadingDialog(context);
|
|
|
|
// Update data di Firebase
|
|
await FirebaseData(siang: jam, siangPutaran: putaran).toJson();
|
|
Navigator.of(context).pop();
|
|
|
|
// Tampilkan toast ketika waktu pakan otomatis telah tiba
|
|
scheduleNotification(picked,
|
|
const Duration(seconds: 5)); // Notifikasi 5 detik lebih lambat
|
|
|
|
reload();
|
|
}
|
|
}
|
|
|
|
bool isTimeInSiangRange(TimeOfDay time) {
|
|
// Rentang waktu siang adalah dari jam 11:00 hingga 15:59
|
|
return time.hour >= 11 && time.hour < 16;
|
|
}
|
|
|
|
Future<void> _setMalam() async {
|
|
TimeOfDay? picked = await showTimePicker(
|
|
context: context,
|
|
initialTime: _timeOfDay,
|
|
);
|
|
|
|
if (picked != null) {
|
|
// Validasi waktu yang dipilih
|
|
if (!isTimeInMalamRange(picked)) {
|
|
// Gunakan Builder widget untuk mendapatkan BuildContext
|
|
showDialog(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text('Pilih waktu antara 16:00 hingga 02:59 untuk malam'),
|
|
actions: <Widget>[
|
|
TextButton(
|
|
child: Text('OK'),
|
|
onPressed: () {
|
|
Navigator.of(context).pop();
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
return;
|
|
}
|
|
|
|
String jam =
|
|
'${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}';
|
|
int putaran = await _showPutaranDialog();
|
|
showLoadingDialog(context);
|
|
|
|
// Update data di Firebase
|
|
await FirebaseData(malam: jam, malamPutaran: putaran).toJson();
|
|
Navigator.of(context).pop();
|
|
|
|
// Tampilkan toast ketika waktu pakan otomatis telah tiba
|
|
scheduleNotification(picked,
|
|
const Duration(seconds: 5)); // Notifikasi 5 detik lebih lambat
|
|
|
|
reload();
|
|
}
|
|
}
|
|
|
|
bool isTimeInMalamRange(TimeOfDay time) {
|
|
// Rentang waktu malam dari jam 16:00 hingga 23:59 dan dari 00:00 hingga 02:59
|
|
return (time.hour >= 16 && time.hour <= 23) ||
|
|
(time.hour >= 0 && time.hour < 3);
|
|
}
|
|
|
|
int getPutaranForTime(TimeOfDay time) {
|
|
if (time.hour >= 3 && time.hour < 11) {
|
|
return pagiPutaran;
|
|
} else if (time.hour >= 11 && time.hour < 16) {
|
|
return siangPutaran;
|
|
} else {
|
|
return malamPutaran;
|
|
}
|
|
}
|
|
|
|
void scheduleNotification(TimeOfDay pickedTime, Duration delay) {
|
|
final now = DateTime.now();
|
|
final scheduledTime = DateTime(
|
|
now.year, now.month, now.day, pickedTime.hour, pickedTime.minute);
|
|
|
|
// Jika waktu terjadwal sudah lewat hari ini, pindahkan ke hari berikutnya
|
|
if (scheduledTime.isBefore(now)) {
|
|
scheduledTime.add(Duration(days: 1));
|
|
}
|
|
|
|
// Hitung durasi sampai notifikasi harus ditampilkan
|
|
final timeUntilNotification = scheduledTime.difference(now);
|
|
|
|
// Jika waktu notifikasi sudah lewat, jangan jadwalkan
|
|
if (timeUntilNotification.isNegative) {
|
|
// Misalnya, tampilkan pesan kesalahan atau tangani kasus ini sesuai kebutuhan
|
|
print("Waktu jadwal pakan sudah lewat. Notifikasi tidak dijadwalkan.");
|
|
return;
|
|
}
|
|
|
|
// Tambahkan delay untuk notifikasi
|
|
Timer(timeUntilNotification + delay, () async {
|
|
// Simpan riwayat pakan otomatis
|
|
await _saveFeedingHistory({
|
|
'time': pickedTime.format(context),
|
|
'putaran': getPutaranForTime(pickedTime),
|
|
// 'type': 'automatic' // Hapus field type
|
|
});
|
|
|
|
// Tampilkan notifikasi hanya setelah pakan benar-benar diberikan
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text(
|
|
'Pakan otomatis pada pukul ${pickedTime.format(context)} telah dilakukan'),
|
|
),
|
|
);
|
|
});
|
|
}
|
|
|
|
Future<void> reload() async {
|
|
final jam = await FirebaseData().getData();
|
|
setState(() {
|
|
pagi = jam.pagi;
|
|
siang = jam.siang;
|
|
malam = jam.malam;
|
|
pagiPutaran = jam.pagiPutaran ?? 0;
|
|
siangPutaran = jam.siangPutaran ?? 0;
|
|
malamPutaran = jam.malamPutaran ?? 0;
|
|
});
|
|
print([pagi, siang, malam, pagiPutaran, siangPutaran, malamPutaran]);
|
|
}
|
|
|
|
Future<int> _showPutaranDialog() async {
|
|
int putaran = 0;
|
|
await showDialog<int>(
|
|
context: context,
|
|
builder: (BuildContext context) {
|
|
return AlertDialog(
|
|
title: Text('Set Putaran Servo'),
|
|
content: TextField(
|
|
keyboardType: TextInputType.number,
|
|
onChanged: (value) {
|
|
putaran = int.tryParse(value) ?? 0;
|
|
},
|
|
decoration: InputDecoration(hintText: "Masukkan jumlah putaran"),
|
|
),
|
|
actions: <Widget>[
|
|
TextButton(
|
|
child: Text('OK'),
|
|
onPressed: () {
|
|
Navigator.of(context).pop(putaran);
|
|
},
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
return putaran;
|
|
}
|
|
|
|
Future<void> _manualFeed() async {
|
|
final now = DateTime.now();
|
|
final formattedTime =
|
|
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}';
|
|
showLoadingDialog(context);
|
|
|
|
try {
|
|
// Menghubungkan ke broker MQTT
|
|
await mqttService.connect();
|
|
// Mengirim pesan manual feed
|
|
await mqttService.sendManualFeedMessage();
|
|
|
|
// Tambahkan penundaan sementara (misalnya 5 detik) sebelum menampilkan notifikasi
|
|
await Future.delayed(const Duration(seconds: 5));
|
|
|
|
// Tutup dialog pemrosesan setelah operasi selesai
|
|
Navigator.of(context).pop();
|
|
|
|
// Simpan riwayat pemberian pakan manual
|
|
await _saveFeedingHistory({
|
|
'time': formattedTime,
|
|
'putaran': 1, // atau nilai yang sesuai
|
|
'type': 'manual'
|
|
});
|
|
|
|
// Tampilkan notifikasi "Pemberian pakan selesai" setelah operasi selesai
|
|
Toast.show("Memberi pakan selesai :)");
|
|
} catch (e) {
|
|
// Tutup dialog pemrosesan jika ada kesalahan
|
|
Navigator.of(context).pop();
|
|
|
|
// Tampilkan notifikasi kesalahan
|
|
Toast.show("Terjadi kesalahan: $e");
|
|
}
|
|
}
|
|
|
|
void showLoadingDialog(BuildContext context) {
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (BuildContext context) {
|
|
return const AlertDialog(
|
|
content: Row(
|
|
children: [
|
|
CircularProgressIndicator(),
|
|
SizedBox(width: 20),
|
|
Text("Memproses..."),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
}
|