1404 lines
48 KiB
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;
|
|
}
|
|
}
|