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 createState() => _CekSensorState(); } class _CekSensorState extends State { 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 _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 _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 _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 _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 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 _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 _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 saveSettings() async { final prefs = await SharedPreferences.getInstance(); await prefs.setDouble('targetTemp', targetTemp); await prefs.setDouble('targetWeightLoss', targetWeightLoss); } Future 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 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 _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; } }