TKK_E32231651/lib/cek_sensor.dart

1404 lines
48 KiB
Dart

import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'global_data.dart';
class CekSensor extends StatefulWidget {
const CekSensor({super.key});
@override
State<CekSensor> createState() => _CekSensorState();
}
class _CekSensorState extends State<CekSensor> {
bool isConnected = false;
Timer? timer;
Timer? _saveTimer;
// Sensor data
double suhu = 0;
double kelembapan = 0;
double berat = 0;
// Control status
bool heater = false;
bool fan = false;
// Mode selection (mutually exclusive) - default 'manual'
String selectedMode = 'manual';
// Keys for SharedPreferences
static const String _keySelectedMode = 'selected_mode';
static const String _keyManualHeater = 'manual_heater';
static const String _keyManualFan = 'manual_fan';
static const String _keyHeater = 'heater_state';
static const String _keyFan = 'fan_state';
// Keys for Drying Tracking
static const String _keyDryingActive = 'drying_active';
static const String _keyDryingStartTime = 'drying_start_time';
static const String _keyInitialWeight = 'initial_weight';
static const String _keyTargetWeight = 'target_weight';
static const String _keyInitialWeightSaved = 'initial_weight_saved';
// Manual mode
bool manualHeater = false;
bool manualFan = false;
// Auto mode
double targetTemp = 35.0;
double targetWeightLoss = 30.0;
double initialWeight = 0;
double targetWeight = 0;
bool initialWeightSaved = false;
// Drying tracking
DateTime? dryingStartTime;
Duration dryingDuration = Duration.zero;
bool dryingActive = false;
// Controllers
final tempController = TextEditingController();
final weightController = TextEditingController();
@override
void initState() {
super.initState();
_loadPersistentState();
_loadDryingState(); // Load drying state FIRST
_loadSettings();
_loadHistory();
_startSensorReading();
_startSaveTimer();
_startDryingTimer();
}
// Load drying state from SharedPreferences
Future<void> _loadDryingState() async {
final prefs = await SharedPreferences.getInstance();
setState(() {
dryingActive = prefs.getBool(_keyDryingActive) ?? false;
initialWeightSaved = prefs.getBool(_keyInitialWeightSaved) ?? false;
initialWeight = prefs.getDouble(_keyInitialWeight) ?? 0;
targetWeight = prefs.getDouble(_keyTargetWeight) ?? 0;
String? startTimeStr = prefs.getString(_keyDryingStartTime);
if (startTimeStr != null && dryingActive) {
dryingStartTime = DateTime.parse(startTimeStr);
// Hitung ulang durasi berdasarkan waktu yang sudah berlalu
dryingDuration = DateTime.now().difference(dryingStartTime!);
} else {
dryingStartTime = null;
dryingDuration = Duration.zero;
}
});
print(
'Loaded drying state - Active: $dryingActive, Duration: ${_formatDuration(dryingDuration)}');
}
// Save drying state to SharedPreferences
Future<void> _saveDryingState() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_keyDryingActive, dryingActive);
await prefs.setBool(_keyInitialWeightSaved, initialWeightSaved);
await prefs.setDouble(_keyInitialWeight, initialWeight);
await prefs.setDouble(_keyTargetWeight, targetWeight);
if (dryingStartTime != null) {
await prefs.setString(
_keyDryingStartTime, dryingStartTime!.toIso8601String());
} else {
await prefs.remove(_keyDryingStartTime);
}
print(
'Saved drying state - Active: $dryingActive, Duration: ${_formatDuration(dryingDuration)}');
}
Future<void> _loadPersistentState() async {
final prefs = await SharedPreferences.getInstance();
selectedMode = prefs.getString(_keySelectedMode) ?? 'manual';
manualHeater = prefs.getBool(_keyManualHeater) ?? false;
manualFan = prefs.getBool(_keyManualFan) ?? false;
heater = prefs.getBool(_keyHeater) ?? false;
fan = prefs.getBool(_keyFan) ?? false;
if (selectedMode == 'manual') {
manualHeater = heater;
manualFan = fan;
}
print(
'Loaded persistent state - Mode: $selectedMode, Heater: $heater, Fan: $fan');
}
Future<void> _savePersistentState() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(_keySelectedMode, selectedMode);
await prefs.setBool(_keyManualHeater, manualHeater);
await prefs.setBool(_keyManualFan, manualFan);
await prefs.setBool(_keyHeater, heater);
await prefs.setBool(_keyFan, fan);
}
void _startSensorReading() {
getSensor();
getControlStatus();
timer = Timer.periodic(const Duration(seconds: 3), (timer) => getSensor());
}
void _startSaveTimer() {
_saveTimer?.cancel();
int intervalSeconds = _getIntervalInSeconds();
_saveTimer =
Timer.periodic(Duration(seconds: intervalSeconds), (timer) async {
if (isConnected) {
Map<String, dynamic> sensorData = {
'suhu': suhu,
'kelembapan': kelembapan,
'berat': berat,
};
await HistoryManager.addData(sensorData);
print('History saved at interval: $intervalSeconds seconds');
}
});
}
void _restartSaveTimer() {
_startSaveTimer();
}
void _startDryingTimer() {
Timer.periodic(const Duration(seconds: 1), (timer) {
if (mounted && dryingActive && dryingStartTime != null) {
setState(() {
dryingDuration = DateTime.now().difference(dryingStartTime!);
});
// Periodically save duration (every 10 seconds)
if (dryingDuration.inSeconds % 10 == 0) {
_saveDryingState();
}
}
});
}
void _startDryingTracking() {
if (!dryingActive) {
setState(() {
dryingActive = true;
dryingStartTime = DateTime.now();
dryingDuration = Duration.zero;
});
_saveDryingState();
print('Drying tracking started');
}
}
void _stopDryingTracking() {
if (dryingActive) {
setState(() {
dryingActive = false;
dryingStartTime = null;
dryingDuration = Duration.zero;
});
_saveDryingState();
print('Drying tracking stopped');
}
}
String _formatDuration(Duration duration) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
String hours = twoDigits(duration.inHours);
String minutes = twoDigits(duration.inMinutes.remainder(60));
String seconds = twoDigits(duration.inSeconds.remainder(60));
if (duration.inHours > 0) {
return '$hours:$minutes:$seconds';
} else {
return '$minutes:$seconds';
}
}
Future<void> _loadHistory() async {
await HistoryManager.loadHistory();
if (mounted) {
setState(() {});
}
}
int _getIntervalInSeconds() {
switch (HistoryManager.intervalUnit) {
case 'detik':
return HistoryManager.intervalValue;
case 'menit':
return HistoryManager.intervalValue * 60;
case 'jam':
return HistoryManager.intervalValue * 3600;
default:
return 5;
}
}
Future<void> _loadSettings() async {
final prefs = await SharedPreferences.getInstance();
if (mounted) {
setState(() {
targetTemp = prefs.getDouble('targetTemp') ?? 35.0;
targetWeightLoss = prefs.getDouble('targetWeightLoss') ?? 30.0;
tempController.text = targetTemp.toString();
weightController.text = targetWeightLoss.toString();
});
}
}
Future<void> saveSettings() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setDouble('targetTemp', targetTemp);
await prefs.setDouble('targetWeightLoss', targetWeightLoss);
}
Future<void> getControlStatus() async {
try {
final response = await http.get(
Uri.parse('http://192.168.100.9/kopikaocare_api/get_control.php'),
);
if (response.statusCode == 200 && mounted) {
final data = jsonDecode(response.body);
setState(() {
heater = data['heater'] == 1;
fan = data['fan'] == 1;
if (data.containsKey('mode')) {
selectedMode = data['mode'] ?? 'manual';
}
if (data.containsKey('manual_heater')) {
manualHeater = data['manual_heater'] == 1;
}
if (data.containsKey('manual_fan')) {
manualFan = data['manual_fan'] == 1;
}
});
await _savePersistentState();
print(
'Control status loaded - Heater: $heater, Fan: $fan, Mode: $selectedMode');
}
} catch (e) {
debugPrint('GET CONTROL ERROR: $e');
}
}
Future<void> getSensor() async {
try {
final response = await http.get(
Uri.parse('http://192.168.100.9/kopikaocare_api/get_sensor.php'),
);
if (response.statusCode != 200) {
if (mounted) setState(() => isConnected = false);
return;
}
final data = jsonDecode(response.body);
if (data['status'] != 'success') {
if (mounted) setState(() => isConnected = false);
return;
}
final sensor = data['data'];
if (mounted) {
setState(() {
suhu = double.tryParse(sensor['suhu'].toString()) ?? 0;
kelembapan = double.tryParse(sensor['kelembapan'].toString()) ?? 0;
berat = double.tryParse(sensor['berat'].toString()) ?? 0;
if (berat < 0) berat = 0;
isConnected = true;
});
}
if (selectedMode == 'auto') {
_runAutoMode();
}
} catch (e) {
if (mounted) setState(() => isConnected = false);
}
}
void _runAutoMode() {
if (!initialWeightSaved && berat > 50) {
setState(() {
initialWeight = berat;
targetWeight = initialWeight * (1 - targetWeightLoss / 100);
initialWeightSaved = true;
});
_startDryingTracking();
print(
'Auto mode: Initial weight saved: $initialWeight, Target: $targetWeight');
}
bool newHeater = heater;
bool newFan = fan;
if (berat <= 1) {
newHeater = false;
newFan = false;
if (dryingActive) {
_stopDryingTracking();
}
} else {
if (initialWeightSaved && initialWeight > 0) {
newHeater = berat > targetWeight;
} else if (berat > 50) {
newHeater = true;
} else {
newHeater = false;
}
}
newFan = suhu >= targetTemp;
if (newHeater != heater || newFan != fan) {
setState(() {
heater = newHeater;
fan = newFan;
});
_updateControl();
_savePersistentState();
}
if (initialWeightSaved && berat <= targetWeight && dryingActive) {
_turnOffAllDevices();
_stopDryingTracking();
_showDryingCompleteDialog();
}
}
void _turnOffAllDevices() {
setState(() {
heater = false;
fan = false;
if (selectedMode == 'manual') {
manualHeater = false;
manualFan = false;
}
});
_updateControl();
_savePersistentState();
_saveDryingState();
}
Future<void> _updateControl() async {
try {
final response = await http.post(
Uri.parse('http://192.168.100.9/kopikaocare_api/update_control.php'),
body: {
"heater": heater ? "1" : "0",
"fan": fan ? "1" : "0",
"mode": selectedMode,
"manual_heater": manualHeater ? "1" : "0",
"manual_fan": manualFan ? "1" : "0",
},
);
if (response.statusCode == 200) {
final data = jsonDecode(response.body);
print('Control updated: ${data['status']}');
}
} catch (e) {
print('UPDATE CONTROL ERROR: $e');
}
}
void _enableManualMode() {
_turnOffAllDevices();
setState(() {
selectedMode = 'manual';
initialWeightSaved = false;
initialWeight = 0;
targetWeight = 0;
manualHeater = false;
manualFan = false;
heater = false;
fan = false;
_stopDryingTracking();
});
_updateControl();
_savePersistentState();
_saveDryingState();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Mode Manual Aktif - Semua perangkat dimatikan'),
backgroundColor: Colors.blue,
duration: Duration(seconds: 2),
),
);
}
void _enableAutoMode() {
_turnOffAllDevices();
setState(() {
selectedMode = 'auto';
manualHeater = false;
manualFan = false;
initialWeightSaved = false;
initialWeight = 0;
targetWeight = 0;
heater = false;
fan = false;
_stopDryingTracking();
});
_updateControl();
_savePersistentState();
_saveDryingState();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Mode Auto Aktif - Semua perangkat dimatikan'),
backgroundColor: Colors.red,
duration: Duration(seconds: 2),
),
);
}
void _toggleManualHeater() {
if (selectedMode != 'manual') return;
setState(() {
manualHeater = !manualHeater;
heater = manualHeater;
});
_updateControl();
_savePersistentState();
if (heater || fan) {
_startDryingTracking();
} else {
_stopDryingTracking();
}
_saveDryingState();
}
void _toggleManualFan() {
if (selectedMode != 'manual') return;
setState(() {
manualFan = !manualFan;
fan = manualFan;
});
_updateControl();
_savePersistentState();
if (heater || fan) {
_startDryingTracking();
} else {
_stopDryingTracking();
}
_saveDryingState();
}
void resetWeight() {
setState(() {
initialWeightSaved = false;
initialWeight = 0;
targetWeight = 0;
_stopDryingTracking();
});
_saveDryingState();
}
void _showDryingCompleteDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
title: const Icon(Icons.check_circle, color: Colors.green, size: 40),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('Pengeringan Selesai!',
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18)),
const SizedBox(height: 10),
Text('Total durasi: ${_formatDuration(dryingDuration)}'),
const SizedBox(height: 5),
const Text('Target berat telah tercapai.'),
],
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
void showSettingsDialog() {
tempController.text = targetTemp.toString();
weightController.text = targetWeightLoss.toString();
showDialog(
context: context,
builder: (context) => StatefulBuilder(
builder: (context, setStateDialog) {
return AlertDialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(25)),
title: const Row(children: [
Icon(Icons.settings_suggest, color: Colors.red),
SizedBox(width: 10),
Text('Auto Mode Settings')
]),
content: Column(mainAxisSize: MainAxisSize.min, children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(15)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(children: [
Icon(Icons.air, color: Colors.blue),
SizedBox(width: 8),
Text('Fan Control',
style: TextStyle(fontWeight: FontWeight.bold))
]),
const SizedBox(height: 8),
Text(
'Fan ON saat suhu ≥ ${targetTemp.toStringAsFixed(0)}°C'),
Row(children: [
Expanded(
child: Slider(
value: targetTemp,
min: 20,
max: 60,
activeColor: Colors.blue,
onChanged: (v) {
setStateDialog(() {
targetTemp = v;
tempController.text = v.toStringAsFixed(0);
});
})),
Container(
width: 60,
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(8)),
child: Text('${targetTemp.toStringAsFixed(0)}°C',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white))),
]),
],
),
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(15)),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Row(children: [
Icon(Icons.whatshot, color: Colors.red),
SizedBox(width: 8),
Text('Heater Control',
style: TextStyle(fontWeight: FontWeight.bold))
]),
const SizedBox(height: 8),
Text(
'Heater OFF setelah berat turun ${targetWeightLoss.toStringAsFixed(0)}%'),
Row(children: [
Expanded(
child: Slider(
value: targetWeightLoss,
min: 10,
max: 80,
activeColor: Colors.red,
onChanged: (v) {
setStateDialog(() {
targetWeightLoss = v;
weightController.text = v.toStringAsFixed(0);
});
})),
Container(
width: 60,
padding: const EdgeInsets.symmetric(vertical: 6),
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(8)),
child: Text('${targetWeightLoss.toStringAsFixed(0)}%',
textAlign: TextAlign.center,
style: const TextStyle(color: Colors.white))),
]),
],
),
),
]),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('Batal')),
ElevatedButton(
onPressed: () async {
setState(() {
targetTemp = double.parse(tempController.text);
targetWeightLoss = double.parse(weightController.text);
});
await saveSettings();
if (mounted) Navigator.pop(context);
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.red),
child: const Text('Simpan')),
],
);
},
),
);
}
@override
void dispose() {
_savePersistentState();
_saveDryingState();
timer?.cancel();
_saveTimer?.cancel();
tempController.dispose();
weightController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: const Color(0xfff8f9fa),
appBar: AppBar(
title: const Text('Sensor Monitoring',
style: TextStyle(fontWeight: FontWeight.bold)),
backgroundColor: Colors.red,
foregroundColor: Colors.white,
actions: [
IconButton(
icon: const Icon(Icons.settings_suggest),
onPressed: showSettingsDialog,
tooltip: 'Settings Auto Mode'),
IconButton(
icon: const Icon(Icons.history),
onPressed: () => Navigator.pushNamed(context, '/history'),
tooltip: 'Riwayat Sensor'),
Container(
margin: const EdgeInsets.only(right: 16),
child: Chip(
label: Text(isConnected ? 'ONLINE' : 'OFFLINE',
style: TextStyle(
color: isConnected ? Colors.white : Colors.red)),
backgroundColor: isConnected ? Colors.green : Colors.white)),
],
),
body: RefreshIndicator(
onRefresh: () async {
await getSensor();
await getControlStatus();
},
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(children: [
_buildHeaderCard(),
const SizedBox(height: 20),
_buildModeSelector(),
const SizedBox(height: 20),
if (selectedMode == 'auto') _buildAutoModeCard(),
if (selectedMode == 'manual') _buildManualModeCard(),
const SizedBox(height: 20),
_buildDryingCard(),
const SizedBox(height: 25),
const Text('Sensor Data',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 15),
_buildSensorCard('Temperature', suhu, '°C', Icons.thermostat,
_getTemperatureColor(suhu), 0, 60, targetTemp),
const SizedBox(height: 15),
_buildSensorCard('Humidity', kelembapan, '%', Icons.water_drop,
_getHumidityColor(kelembapan), 0, 100, null),
const SizedBox(height: 15),
_buildWeightCard(),
const SizedBox(height: 25),
_buildRelayStatusCard(),
const SizedBox(height: 25),
Center(
child: Column(
children: [
Text(
'Last update: ${DateTime.now().hour.toString().padLeft(2, '0')}:${DateTime.now().minute.toString().padLeft(2, '0')}:${DateTime.now().second.toString().padLeft(2, '0')}',
style:
TextStyle(fontSize: 11, color: Colors.grey.shade500)),
const SizedBox(height: 4),
Text(
'Interval: ${HistoryManager.intervalValue} ${HistoryManager.intervalUnit}',
style:
TextStyle(fontSize: 10, color: Colors.grey.shade400)),
Text('History: ${historyGlobal.length} data',
style:
TextStyle(fontSize: 10, color: Colors.grey.shade400)),
],
)),
]),
),
),
);
}
Widget _buildModeSelector() {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(15),
boxShadow: [
BoxShadow(blurRadius: 10, color: Colors.black.withOpacity(0.05))
],
),
child: Row(
children: [
_buildModeButton('auto', 'AUTO MODE', Icons.smart_toy, Colors.red,
_enableAutoMode),
_buildModeButton('manual', 'MANUAL MODE', Icons.touch_app,
Colors.blue, _enableManualMode),
],
),
);
}
Widget _buildModeButton(String mode, String label, IconData icon, Color color,
VoidCallback onTap) {
bool isSelected = selectedMode == mode;
return Expanded(
child: GestureDetector(
onTap: onTap,
child: Container(
padding: const EdgeInsets.symmetric(vertical: 14),
decoration: BoxDecoration(
color: isSelected ? color.withOpacity(0.1) : Colors.transparent,
borderRadius: BorderRadius.circular(15),
border:
Border.all(color: isSelected ? color : Colors.grey.shade300),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, color: isSelected ? color : Colors.grey, size: 22),
const SizedBox(width: 8),
Text(label,
style: TextStyle(
color: isSelected ? color : Colors.grey,
fontWeight:
isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 14)),
],
),
),
),
);
}
Widget _buildAutoModeCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.red.shade50,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.red.shade200),
),
child: Column(
children: [
const Row(
children: [
Icon(Icons.smart_toy, color: Colors.red),
SizedBox(width: 8),
Text('Mode Auto Aktif',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.red)),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10)),
child: Column(
children: [
const Text('Fan ON',
style: TextStyle(fontSize: 12, color: Colors.grey)),
Text('${targetTemp.toStringAsFixed(0)}°C',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.blue)),
],
),
),
),
const SizedBox(width: 12),
Expanded(
child: Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10)),
child: Column(
children: [
const Text('Heater OFF',
style: TextStyle(fontSize: 12, color: Colors.grey)),
Text('turun ${targetWeightLoss.toStringAsFixed(0)}%',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.red)),
],
),
),
),
],
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8)),
child: const Row(
children: [
Icon(Icons.info, size: 16, color: Colors.blue),
SizedBox(width: 8),
Expanded(
child: Text(
'Auto mode akan aktif otomatis saat berat > 50g. Kontrol penuh oleh sistem berdasarkan suhu dan berat.',
style: TextStyle(fontSize: 11, color: Colors.blue),
),
),
],
),
),
if (initialWeightSaved && selectedMode == 'auto') ...[
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(8)),
child: Row(
children: [
const Icon(Icons.check_circle, size: 16, color: Colors.green),
const SizedBox(width: 8),
Expanded(
child: Text(
'Berat awal: ${initialWeight.toStringAsFixed(0)}g → Target: ${targetWeight.toStringAsFixed(0)}g',
style: const TextStyle(fontSize: 11, color: Colors.green),
),
),
GestureDetector(
onTap: resetWeight,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange,
borderRadius: BorderRadius.circular(15)),
child: const Text('Reset',
style: TextStyle(fontSize: 10, color: Colors.white)),
),
),
],
),
),
],
],
),
);
}
Widget _buildManualModeCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(15),
border: Border.all(color: Colors.blue.shade200),
),
child: Column(
children: [
const Row(
children: [
Icon(Icons.touch_app, color: Colors.blue),
SizedBox(width: 8),
Text('Mode Manual',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: Colors.blue)),
],
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
onPressed: _toggleManualHeater,
icon: Icon(
manualHeater ? Icons.whatshot : Icons.whatshot_outlined),
label: Text(manualHeater ? 'HEATER ON' : 'HEATER OFF'),
style: ElevatedButton.styleFrom(
backgroundColor: manualHeater ? Colors.red : Colors.grey,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton.icon(
onPressed: _toggleManualFan,
icon: Icon(manualFan ? Icons.air : Icons.air_outlined),
label: Text(manualFan ? 'FAN ON' : 'FAN OFF'),
style: ElevatedButton.styleFrom(
backgroundColor: manualFan ? Colors.cyan : Colors.grey,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
),
),
),
],
),
const SizedBox(height: 12),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.5),
borderRadius: BorderRadius.circular(8)),
child: const Row(
children: [
Icon(Icons.touch_app, size: 16, color: Colors.blue),
SizedBox(width: 8),
Expanded(
child: Text(
'Kontrol manual Heater dan Fan. Mode Auto tidak berjalan saat Mode Manual aktif.',
style: TextStyle(fontSize: 11, color: Colors.blue),
),
),
],
),
),
],
),
);
}
Widget _buildDryingCard() {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: dryingActive
? [Colors.green.shade700, Colors.green.shade500]
: [Colors.grey.shade600, Colors.grey.shade500],
),
borderRadius: BorderRadius.circular(15),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(dryingActive ? Icons.play_circle : Icons.stop_circle,
color: Colors.white),
const SizedBox(width: 8),
Text(
dryingActive ? 'PENGERINGAN BERJALAN' : 'PENGERINGAN STOP',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
fontSize: 14),
),
],
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.2),
borderRadius: BorderRadius.circular(15)),
child: Text(
selectedMode == 'auto' ? 'MODE AUTO' : 'MODE MANUAL',
style: const TextStyle(
color: Colors.white,
fontSize: 10,
fontWeight: FontWeight.bold),
),
),
],
),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
Column(
children: [
const Text('Durasi',
style: TextStyle(color: Colors.white70, fontSize: 10)),
Text(
_formatDuration(dryingDuration),
style: const TextStyle(
color: Colors.white,
fontSize: 20,
fontWeight: FontWeight.bold,
fontFamily: 'monospace'),
),
],
),
Container(
width: 1, height: 40, color: Colors.white.withOpacity(0.3)),
Column(
children: [
const Text('Status',
style: TextStyle(color: Colors.white70, fontSize: 10)),
Text(
dryingActive ? 'AKTIF' : 'NONAKTIF',
style: TextStyle(
color:
dryingActive ? Colors.lightGreen : Colors.white70,
fontSize: 16,
fontWeight: FontWeight.bold),
),
],
),
],
),
],
),
);
}
Widget _buildHeaderCard() => Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.red.shade700, Colors.red.shade500]),
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(blurRadius: 20, color: Colors.red.withOpacity(0.3))
]),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('Live Monitoring',
style: TextStyle(color: Colors.white, fontSize: 14)),
const Text('Real-time Sensor Data',
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.bold)),
const SizedBox(height: 20),
Row(mainAxisAlignment: MainAxisAlignment.spaceAround, children: [
_buildMiniStat(
Icons.thermostat, '${suhu.toStringAsFixed(1)}°C', 'Suhu'),
_buildMiniStat(Icons.water_drop, '${kelembapan.toStringAsFixed(1)}%',
'Kelembapan'),
_buildMiniStat(
Icons.fitness_center, '${berat.toStringAsFixed(0)}g', 'Berat'),
]),
]));
Widget _buildMiniStat(IconData icon, String value, String label) =>
Column(children: [
Icon(icon, color: Colors.white, size: 28),
const SizedBox(height: 5),
Text(value,
style: const TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.bold)),
Text(label,
style:
TextStyle(color: Colors.white.withOpacity(0.8), fontSize: 11))
]);
Widget _buildSensorCard(String title, double value, String unit,
IconData icon, Color color, double min, double max, double? target) {
double progress = ((value - min) / (max - min)).clamp(0.0, 1.0);
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(blurRadius: 10, color: Colors.black.withOpacity(0.05))
]),
child: Padding(
padding: const EdgeInsets.all(16),
child:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12)),
child: Icon(icon, color: color, size: 24)),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
fontSize: 14, color: Color(0xff666666))),
Text('$value $unit',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color))
])),
if (target != null)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: Colors.orange.withOpacity(0.1),
borderRadius: BorderRadius.circular(15)),
child: Text('Target: ${target.toStringAsFixed(0)}$unit',
style: const TextStyle(
fontSize: 10,
color: Colors.orange,
fontWeight: FontWeight.bold))),
]),
const SizedBox(height: 15),
LinearProgressIndicator(
value: progress,
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation(color),
minHeight: 8,
borderRadius: BorderRadius.circular(4)),
])));
}
Widget _buildWeightCard() {
double progress =
initialWeightSaved ? (berat / initialWeight).clamp(0.0, 1.0) : 0;
Color color = _getWeightColor(berat);
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(blurRadius: 10, color: Colors.black.withOpacity(0.05))
]),
child: Padding(
padding: const EdgeInsets.all(16),
child:
Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
Row(children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: color.withOpacity(0.1),
borderRadius: BorderRadius.circular(12)),
child: const Icon(Icons.fitness_center, size: 24)),
const SizedBox(width: 12),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text('Weight',
style: TextStyle(
fontSize: 14, color: Color(0xff666666))),
Text('${berat.toStringAsFixed(0)} g',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: color)),
])),
]),
if (initialWeightSaved && selectedMode == 'auto') ...[
const SizedBox(height: 15),
LinearProgressIndicator(
value: progress,
backgroundColor: Colors.grey.shade200,
valueColor: AlwaysStoppedAnimation(color),
minHeight: 8,
borderRadius: BorderRadius.circular(4)),
const SizedBox(height: 8),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('0 g',
style: TextStyle(
fontSize: 10, color: Colors.grey.shade500)),
Text('Target: ${targetWeight.toStringAsFixed(0)} g',
style: TextStyle(
fontSize: 10,
color: Colors.orange.shade700,
fontWeight: FontWeight.w500)),
Text('${initialWeight.toStringAsFixed(0)} g',
style: TextStyle(
fontSize: 10, color: Colors.grey.shade500)),
]),
const SizedBox(height: 8),
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(10)),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Penurunan',
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600)),
Text(
'${((initialWeight - berat) / initialWeight * 100).toStringAsFixed(1)}%',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.green)),
]),
])),
],
if (selectedMode == 'auto' &&
!initialWeightSaved &&
berat > 0 &&
berat < 50)
Container(
margin: const EdgeInsets.only(top: 12),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8)),
child: const Row(
children: [
Icon(Icons.info, size: 16, color: Colors.blue),
SizedBox(width: 8),
Expanded(
child: Text(
'Auto mode akan aktif saat berat mencapai 50g',
style:
TextStyle(fontSize: 11, color: Colors.blue))),
],
),
),
if (selectedMode == 'manual' && (heater || fan))
Container(
margin: const EdgeInsets.only(top: 12),
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.orange.shade50,
borderRadius: BorderRadius.circular(8)),
child: const Row(
children: [
Icon(Icons.info, size: 16, color: Colors.orange),
SizedBox(width: 8),
Expanded(
child: Text(
'Mode Manual: Kontrol perangkat secara manual',
style: TextStyle(
fontSize: 11, color: Colors.orange))),
],
),
),
])));
}
Widget _buildRelayStatusCard() => Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(blurRadius: 15, color: Colors.black.withOpacity(0.05))
]),
child: Padding(
padding: const EdgeInsets.all(20),
child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
const Text('Relay Status',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Color(0xff666666))),
const SizedBox(height: 10),
Row(children: [
_buildRelayStatus('Heater', heater, Icons.whatshot, Colors.red),
const SizedBox(width: 20),
_buildRelayStatus('Fan', fan, Icons.air, Colors.cyan)
]),
]),
));
Widget _buildRelayStatus(
String title, bool isOn, IconData icon, Color color) =>
Expanded(
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: isOn ? color.withOpacity(0.1) : Colors.grey.shade100,
borderRadius: BorderRadius.circular(12),
border:
Border.all(color: isOn ? color : Colors.grey.shade300)),
child: Column(children: [
Icon(icon, color: isOn ? color : Colors.grey, size: 24),
const SizedBox(height: 5),
Text(title,
style: TextStyle(
fontSize: 12,
color: isOn ? color : Colors.grey,
fontWeight: FontWeight.w500)),
Container(
width: 8,
height: 8,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isOn ? color : Colors.grey)),
])));
Color _getTemperatureColor(double temp) => temp < 20
? Colors.blue
: (temp < targetTemp
? Colors.green
: (temp < targetTemp + 5 ? Colors.orange : Colors.red));
Color _getHumidityColor(double humidity) => humidity < 30
? Colors.orange
: (humidity < 70 ? Colors.green : Colors.blue);
Color _getWeightColor(double weight) {
if (!initialWeightSaved || selectedMode != 'auto') return Colors.grey;
if (weight <= targetWeight) return Colors.green;
if (weight <= initialWeight * 0.7) return Colors.orange;
return Colors.red;
}
}