commit 864ec675fd34d212a71ce20768a86181b40a93fc Author: Rafiabiyyuy Date: Tue Aug 19 14:06:19 2025 +0700 main diff --git a/main.dart b/main.dart new file mode 100644 index 0000000..c5210c5 --- /dev/null +++ b/main.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'screens/realtime_data_screen.dart'; +import 'screens/pump_schedule_screen.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + try { + if (kIsWeb) { + await Firebase.initializeApp( + options: const FirebaseOptions( + apiKey: "AIzaSyBCe-30GPJRp0p7psDH4t2D01WHPcSNIqQ", + authDomain: "apkrafi.firebaseapp.com", + databaseURL: "https://apkrafi-default-rtdb.firebaseio.com", + projectId: "apkrafi", + storageBucket: "apkrafi.appspot.com", + messagingSenderId: "121136047777", + appId: "1:121136047777:web:306ae9d0b88b7c33c432e0", + measurementId: "G-L31DDT24WJ"), + ); + } else { + await Firebase.initializeApp(); + } + print('Firebase initialized successfully'); + } catch (e) { + print('Error initializing Firebase: $e'); + } + + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({Key? key}) : super(key: key); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Smart Garden Monitor', + theme: ThemeData( + primarySwatch: Colors.green, + visualDensity: VisualDensity.adaptivePlatformDensity, + ), + home: const MainScreen(), + ); + } +} + +class MainScreen extends StatefulWidget { + const MainScreen({Key? key}) : super(key: key); + + @override + State createState() => _MainScreenState(); +} + +class _MainScreenState extends State { + int _selectedIndex = 0; + + final List _screens = [ + const RealtimeDataScreen(), + const PumpScheduleScreen(), + ]; + + @override + Widget build(BuildContext context) { + return Scaffold( + body: _screens[_selectedIndex], + bottomNavigationBar: BottomNavigationBar( + currentIndex: _selectedIndex, + onTap: (index) { + setState(() { + _selectedIndex = index; + }); + }, + items: const [ + BottomNavigationBarItem( + icon: Icon(Icons.sensors), + label: 'Sensor', + ), + BottomNavigationBarItem( + icon: Icon(Icons.schedule), + label: 'Jadwal', + ), + ], + ), + ); + } +} diff --git a/pump_control_screen.dart b/pump_control_screen.dart new file mode 100644 index 0000000..920ebf4 --- /dev/null +++ b/pump_control_screen.dart @@ -0,0 +1,348 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_database/firebase_database.dart'; + +class PumpControlScreen extends StatefulWidget { + const PumpControlScreen({Key? key}) : super(key: key); + + @override + State createState() => _PumpControlScreenState(); +} + +class _PumpControlScreenState extends State { + final DatabaseReference _database = FirebaseDatabase.instance.ref(); + bool pump3Enabled = false; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _loadPumpStatus(); + } + + void _loadPumpStatus() { + _database.child('schedule/pump3Enabled').onValue.listen((event) { + if (event.snapshot.value != null) { + setState(() { + pump3Enabled = event.snapshot.value as bool; + isLoading = false; + }); + print( + 'Pump status updated: pump3Enabled = $pump3Enabled'); // Debug print + } + }); + } + + Future _togglePump3(bool value) async { + try { + await _database.child('schedule').update({ + 'pump3Enabled': value, + }); + print('Updated pump3 status: $value'); // Debug print + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Pompa 3 ${value ? "diaktifkan" : "dinonaktifkan"}'), + backgroundColor: Colors.green, + ), + ); + } catch (e) { + print('Error updating pump status: $e'); // Debug print + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + + Widget _buildPump2Card() { + return Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + child: Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.grey.shade200, Colors.grey.shade100], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(15), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.water, + color: Colors.grey, + size: 32, + ), + const SizedBox(width: 12), + Text( + 'Pompa 2', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.grey, + ), + ), + ], + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + Icons.power_off, + color: Colors.grey, + ), + const SizedBox(width: 8), + Text( + 'Pompa Mati', + style: TextStyle( + color: Colors.grey, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildPump3Card() { + return Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + child: Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [Colors.blue.shade200, Colors.blue.shade100], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(15), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.water, + color: Colors.blue, + size: 32, + ), + const SizedBox(width: 12), + Text( + 'Pompa 3', + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.blue, + ), + ), + ], + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: pump3Enabled + ? Colors.blue.withOpacity(0.1) + : Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + pump3Enabled ? Icons.power : Icons.power_off, + color: pump3Enabled ? Colors.blue : Colors.grey, + ), + const SizedBox(width: 8), + Text( + pump3Enabled ? 'Pompa Menyala' : 'Pompa Mati', + style: TextStyle( + color: pump3Enabled ? Colors.blue : Colors.grey, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + Widget _buildPump4Card() { + return Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + child: Container( + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + !pump3Enabled ? Colors.green.shade200 : Colors.grey.shade200, + !pump3Enabled ? Colors.green.shade100 : Colors.grey.shade100 + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(15), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + Icons.water, + color: !pump3Enabled ? Colors.green : Colors.grey, + size: 32, + ), + const SizedBox(width: 12), + Text( + 'Pompa 4', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: !pump3Enabled ? Colors.green : Colors.grey, + ), + ), + ], + ), + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: !pump3Enabled + ? Colors.green.withOpacity(0.1) + : Colors.grey.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + !pump3Enabled ? Icons.power : Icons.power_off, + color: !pump3Enabled ? Colors.green : Colors.grey, + ), + const SizedBox(width: 8), + Text( + !pump3Enabled ? 'Pompa Menyala' : 'Pompa Mati', + style: TextStyle( + color: !pump3Enabled ? Colors.green : Colors.grey, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Kontrol Pompa'), + backgroundColor: Colors.blue, + ), + body: isLoading + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + Expanded( + child: RefreshIndicator( + onRefresh: () async { + setState(() { + isLoading = true; + }); + _loadPumpStatus(); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _buildPump2Card(), + const SizedBox(height: 16), + _buildPump3Card(), + const SizedBox(height: 16), + _buildPump4Card(), + ], + ), + ), + ), + ), + // Toggle button at the bottom + Container( + width: double.infinity, + padding: const EdgeInsets.all(16.0), + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.grey.withOpacity(0.2), + spreadRadius: 1, + blurRadius: 5, + offset: const Offset(0, -2), + ), + ], + ), + child: ElevatedButton( + onPressed: () => _togglePump3(!pump3Enabled), + style: ElevatedButton.styleFrom( + backgroundColor: pump3Enabled ? Colors.red : Colors.green, + padding: const EdgeInsets.symmetric(vertical: 18), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(30), + ), + elevation: 4, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon( + pump3Enabled ? Icons.power_off : Icons.power, + color: Colors.white, + size: 28, + ), + const SizedBox(width: 12), + Text( + pump3Enabled ? 'Matikan Pompa 3' : 'Nyalakan Pompa 3', + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} diff --git a/pump_schedule_screen.dart b/pump_schedule_screen.dart new file mode 100644 index 0000000..c9bf53d --- /dev/null +++ b/pump_schedule_screen.dart @@ -0,0 +1,296 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:intl/intl.dart'; + +class PumpScheduleScreen extends StatefulWidget { + const PumpScheduleScreen({Key? key}) : super(key: key); + + @override + State createState() => _PumpScheduleScreenState(); +} + +class _PumpScheduleScreenState extends State { + final DatabaseReference _database = FirebaseDatabase.instance.ref(); + Map pumpSchedules = {}; + bool isLoading = true; + + @override + void initState() { + super.initState(); + _loadPumpSchedules(); + } + + void _loadPumpSchedules() { + _database.child('schedule').onValue.listen((event) { + if (event.snapshot.value != null) { + setState(() { + pumpSchedules = + Map.from(event.snapshot.value as Map); + isLoading = false; + }); + print('Loaded schedules: $pumpSchedules'); + } + }); + } + + String _formatTime(int hour, int minute) { + final time = DateTime(2024, 1, 1, hour, minute); + return DateFormat('HH:mm').format(time); + } + + Widget _buildPumpScheduleCard(String pumpId, Map schedule) { + final pumpSchedule = schedule[pumpId] as Map? ?? {}; + final startHour = pumpSchedule['startHour'] ?? 0; + final startMinute = pumpSchedule['startMinute'] ?? 0; + final endHour = pumpSchedule['endHour'] ?? 0; + final endMinute = pumpSchedule['endMinute'] ?? 0; + + return Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + pumpId == 'pump2' + ? 'Jadwal Penyiraman' + : pumpId == 'pump3' + ? 'Jadwal Pupuk Pertumbuhan' + : pumpId == 'pump4' + ? 'Jadwal Pupuk Pembuahan' + : 'Pompa $pumpId', + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Waktu Mulai', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + const SizedBox(height: 4), + Text( + _formatTime(startHour, startMinute), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Waktu Selesai', + style: TextStyle( + fontSize: 14, + color: Colors.grey, + ), + ), + const SizedBox(height: 4), + Text( + _formatTime(endHour, endMinute), + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + ], + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + onPressed: () => _showTimePicker(pumpId, true), + icon: const Icon(Icons.edit), + label: const Text('Edit Waktu Mulai'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + ), + ), + const SizedBox(width: 8), + Expanded( + child: ElevatedButton.icon( + onPressed: () => _showTimePicker(pumpId, false), + icon: const Icon(Icons.edit), + label: const Text('Edit Waktu Selesai'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + ), + ), + ), + ], + ), + ], + ), + ), + ); + } + + Future _showTimePicker(String pumpId, bool isStartTime) async { + final currentSchedule = + pumpSchedules[pumpId] as Map? ?? {}; + final currentHour = isStartTime + ? currentSchedule['startHour'] ?? 0 + : currentSchedule['endHour'] ?? 0; + final currentMinute = isStartTime + ? currentSchedule['startMinute'] ?? 0 + : currentSchedule['endMinute'] ?? 0; + + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: TimeOfDay(hour: currentHour, minute: currentMinute), + ); + + if (picked != null) { + final hour = picked.hour; + final minute = picked.minute; + + try { + if (isStartTime) { + await _database.child('schedule/$pumpId').update({ + 'startHour': hour, + 'startMinute': minute, + }); + } else { + await _database.child('schedule/$pumpId').update({ + 'endHour': hour, + 'endMinute': minute, + }); + } + + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Waktu ${isStartTime ? "mulai" : "selesai"} berhasil diperbarui'), + backgroundColor: Colors.green, + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Jadwal Pompa'), + backgroundColor: Colors.green, + ), + body: isLoading + ? const Center(child: CircularProgressIndicator()) + : RefreshIndicator( + onRefresh: () async { + setState(() { + isLoading = true; + }); + _loadPumpSchedules(); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + _buildPumpScheduleCard('pump2', pumpSchedules), + const SizedBox(height: 16), + if (pumpSchedules.containsKey('pump3Enabled')) + Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Text( + 'Jenis Pupuk', + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 8), + ToggleButtons( + isSelected: [ + !(pumpSchedules['pump3Enabled'] ?? false), // false = pembuahan + (pumpSchedules['pump3Enabled'] ?? false), // true = pertumbuhan + ], + onPressed: (index) async { + final newValue = index == 1; // index 1 = pertumbuhan + try { + await _database.child('schedule').update({ + 'pump3Enabled': newValue, + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + newValue + ? 'Mode diubah ke Pupuk Pertumbuhan' + : 'Mode diubah ke Pupuk Pembuahan', + ), + backgroundColor: Colors.green, + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error: ${e.toString()}'), + backgroundColor: Colors.red, + ), + ); + } + }, + borderRadius: BorderRadius.circular(12), + selectedColor: Colors.white, + fillColor: Colors.green, + color: Colors.green, + children: const [ + Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text('Pembuahan'), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Text('Pertumbuhan'), + ), + ], + ), + ], + ), + const SizedBox(height: 16), + _buildPumpScheduleCard('pump3', pumpSchedules), + const SizedBox(height: 16), + _buildPumpScheduleCard('pump4', pumpSchedules), + ], + ), + ), + ), + ); + } +} diff --git a/realtime_data_screen.dart b/realtime_data_screen.dart new file mode 100644 index 0000000..c3ba445 --- /dev/null +++ b/realtime_data_screen.dart @@ -0,0 +1,301 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:firebase_database/firebase_database.dart'; + +class RealtimeDataScreen extends StatefulWidget { + const RealtimeDataScreen({Key? key}) : super(key: key); + + @override + State createState() => _RealtimeDataScreenState(); +} + +class _RealtimeDataScreenState extends State { + final DatabaseReference _database = FirebaseDatabase.instance.ref(); + + Map sensorData = { + 'humidity': 0, + 'soilMoisture': 0, + 'temperature': 0, + }; + bool isLoading = true; + + late Timer _imageRefreshTimer; + String _imageUrl = ''; + String? _scheduledTime; + + @override + void initState() { + super.initState(); + _setupRealtimeListener(); + _startImageAutoRefresh(); + _fetchSchedule(); + } + + void _setupRealtimeListener() { + _database.child('sensors').onValue.listen((event) { + if (event.snapshot.value != null) { + setState(() { + sensorData = Map.from(event.snapshot.value as Map); + isLoading = false; + }); + } + }); + } + + void _startImageAutoRefresh() { + _refreshImageUrl(); + _imageRefreshTimer = Timer.periodic(const Duration(seconds: 10), (timer) { + _refreshImageUrl(); + }); + } + + void _refreshImageUrl() { + setState(() { + _imageUrl = + 'https://nwlikynouhddjwyjrwnl.supabase.co/storage/v1/object/public/photo/latest.jpg?t=${DateTime.now().millisecondsSinceEpoch}'; + }); + } + + Future _fetchSchedule() async { + final snapshot = await _database.child('camera/schedule').get(); + if (snapshot.exists) { + setState(() { + _scheduledTime = snapshot.value.toString().trim().isEmpty + ? null + : snapshot.value.toString(); + }); + } + } + + Future _pickSchedule() async { + final TimeOfDay? picked = await showTimePicker( + context: context, + initialTime: TimeOfDay.now(), + builder: (context, child) { + return MediaQuery( + data: MediaQuery.of(context).copyWith(alwaysUse24HourFormat: true), + child: child!, + ); + }, + ); + + if (picked != null) { + final hour = picked.hour.toString().padLeft(2, '0'); + final minute = picked.minute.toString().padLeft(2, '0'); + final formatted = '$hour.$minute'; + + try { + await _database.child('camera/schedule').set(formatted); + setState(() { + _scheduledTime = formatted; + }); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Gambar dijadwalkan pada $formatted'), + backgroundColor: Colors.green, + ), + ); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Gagal menjadwalkan: $e'), + backgroundColor: Colors.red, + ), + ); + } + } + } + + @override + void dispose() { + _imageRefreshTimer.cancel(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: const Text('Data Sensor & Kamera'), + backgroundColor: Colors.green, + ), + body: isLoading + ? const Center(child: CircularProgressIndicator()) + : RefreshIndicator( + onRefresh: () async { + setState(() { + isLoading = true; + }); + _setupRealtimeListener(); + await _fetchSchedule(); + }, + child: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildSensorCard( + 'Kelembaban Udara', + '${sensorData['humidity']?.toStringAsFixed(1) ?? 'N/A'}%', + Icons.water, + Colors.blue, + ), + const SizedBox(height: 16), + _buildSensorCard( + 'Kelembaban Tanah', + '${sensorData['soilMoisture']?.toStringAsFixed(1) ?? 'N/A'}%', + Icons.water_drop, + Colors.lightBlue, + ), + const SizedBox(height: 16), + _buildSensorCard( + 'Suhu', + '${sensorData['temperature']?.toStringAsFixed(1) ?? 'N/A'}°C', + Icons.thermostat, + Colors.orange, + ), + const SizedBox(height: 24), + + // Combined Container: Jadwal + Lihat Gambar + Container( + padding: const EdgeInsets.all(16), + margin: const EdgeInsets.symmetric(vertical: 8), + decoration: BoxDecoration( + color: Colors.orange.shade50, + borderRadius: BorderRadius.circular(16), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Jadwal Pengambilan Gambar:', + style: TextStyle( + fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + Text( + _scheduledTime ?? 'Belum ada jadwal', + style: const TextStyle( + fontSize: 24, color: Colors.black87), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: ElevatedButton.icon( + icon: const Icon(Icons.schedule), + label: + const Text('Atur Jadwal Pengambilan'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.orange, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 14, horizontal: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + onPressed: _pickSchedule, + ), + ), + const SizedBox(width: 12), + Expanded( + child: ElevatedButton.icon( + icon: const Icon(Icons.image), + label: const Text('Lihat Gambar Terbaru'), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.blue, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric( + vertical: 14, horizontal: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + onPressed: () { + _refreshImageUrl(); + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Gambar Terbaru'), + content: Image.network( + _imageUrl, + fit: BoxFit.contain, + key: ValueKey(_imageUrl), + errorBuilder: + (context, error, stackTrace) => + const Text( + 'Gagal memuat gambar.'), + ), + actions: [ + TextButton( + onPressed: () => + Navigator.of(context).pop(), + child: const Text('Tutup'), + ), + ], + ), + ); + }, + ), + ), + ], + ), + ], + ), + ), + ], + ), + ), + ), + ); + } + + Widget _buildSensorCard( + String title, String value, IconData icon, Color color) { + return Card( + elevation: 4, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(12), + ), + child: Icon(icon, color: color, size: 32), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 4), + Text( + value, + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + ), + ], + ), + ), + ); + } +}