diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 0000000..e549ee2 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/lib/dashboard.dart b/lib/dashboard.dart index 83dd73b..da839f0 100644 --- a/lib/dashboard.dart +++ b/lib/dashboard.dart @@ -1,6 +1,8 @@ import 'dart:ui'; import 'package:flutter/material.dart'; + +import 'firebase_service.dart'; import 'history_page.dart'; class DashboardPage extends StatefulWidget { @@ -19,8 +21,9 @@ class _DashboardPageState extends State int _selectedIndex = 0; late AnimationController _controller; late Animation _fadeAnim; + final FirebaseService _firebaseService = FirebaseService(); - List history = []; + List> history = []; @override void initState() { @@ -34,6 +37,43 @@ class _DashboardPageState extends State curve: Curves.easeOutCubic, ); _controller.forward(); + + // Listen to sensor data + _firebaseService.getSensorData().listen((data) { + if (mounted) { + setState(() { + temperature = data['temperature'] ?? 0.0; + humidity = data['humidity'] ?? 0.0; + }); + } + }); + + // Listen to fan status from status/kipas + _firebaseService.getFanStatus().listen((status) { + if (mounted) { + setState(() { + fanOn = status; + }); + } + }); + + // Listen to pump status from status/pompa + _firebaseService.getPumpStatus().listen((status) { + if (mounted) { + setState(() { + pumpOn = status; + }); + } + }); + + // Listen to history data + _firebaseService.getHistory().listen((data) { + if (mounted) { + setState(() { + history = data; + }); + } + }); } @override @@ -51,19 +91,178 @@ class _DashboardPageState extends State } void _addHistory() { - history.add(HistoryEntry( - time: DateTime.now(), - temperature: temperature, - humidity: humidity, - fanOn: fanOn, - pumpOn: pumpOn, - )); + final historyData = { + 'temperature': temperature, + 'humidity': humidity, + 'fanOn': fanOn, + 'pumpOn': pumpOn, + }; + _firebaseService.addHistory(historyData); + } + + // Update the fan toggle handler + void _toggleFan() { + final newStatus = !fanOn; + setState(() { + fanOn = newStatus; + }); + // Update status/kipas to match the control state + _firebaseService.updateFanStatus(newStatus); + _addHistory(); + } + + // Update the pump toggle handler + void _togglePump() { + final newStatus = !pumpOn; + setState(() { + pumpOn = newStatus; + }); + // Update status/pompa to match the control state + _firebaseService.updatePumpStatus(newStatus); + _addHistory(); + } + + Widget _buildControlButton({ + required bool isOn, + required VoidCallback onTap, + required String label, + required IconData icon, + required Color color, + }) { + return Material( + color: Colors.transparent, + child: InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(5), + child: Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: Offset(0, 3), + ), + ], + ), + child: Padding( + padding: EdgeInsets.all(12), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(5), + ), + child: Icon(icon, color: color, size: 28), + ), + SizedBox(height: 8), + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: color, + ), + ), + SizedBox(height: 8), + AnimatedContainer( + duration: Duration(milliseconds: 200), + padding: EdgeInsets.symmetric(horizontal: 16, vertical: 6), + decoration: BoxDecoration( + color: isOn ? color : Colors.grey[200], + borderRadius: BorderRadius.circular(5), + boxShadow: [ + BoxShadow( + color: (isOn ? color : Colors.grey[300]!).withOpacity( + 0.3, + ), + blurRadius: 4, + offset: Offset(0, 2), + ), + ], + ), + child: Text( + isOn ? 'ON' : 'OFF', + style: TextStyle( + color: isOn ? Colors.white : Colors.grey[600], + fontSize: 12, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + ), + ), + ), + ); + } + + Widget _buildSensorCard({ + required String label, + required double value, + required String unit, + required IconData icon, + required Color color, + }) { + return Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(5), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.05), + blurRadius: 10, + offset: Offset(0, 3), + ), + ], + ), + child: Padding( + padding: EdgeInsets.all(12), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Container( + padding: EdgeInsets.all(12), + decoration: BoxDecoration( + color: color.withOpacity(0.1), + borderRadius: BorderRadius.circular(5), + ), + child: Icon(icon, color: color, size: 28), + ), + SizedBox(height: 8), + Text( + label, + style: TextStyle( + fontSize: 14, + fontWeight: FontWeight.w600, + color: color, + ), + ), + SizedBox(height: 8), + Text( + '${value.toStringAsFixed(1)} $unit', + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: color, + ), + ), + ], + ), + ), + ); } Widget _buildDashboard(BuildContext context) { final size = MediaQuery.of(context).size; final width = size.width; final height = size.height; + // Palet warna const greenPrimary = Color(0xFF1CB56B); const greenGradientStart = Color(0xFF43EA7A); @@ -73,20 +272,14 @@ class _DashboardPageState extends State final time = TimeOfDay.now().format(context); // Header - final headerHeight = height * 0.17; - final logoSize = width * 0.10; - final welcomeFont = width * 0.045; - final timeFont = width * 0.032; + final headerHeight = height * 0.15; + final logoSize = width * 0.08; + final welcomeFont = width * 0.04; + final timeFont = width * 0.03; // Grid final gridPadding = width * 0.04; final gridSpacing = width * 0.03; - final cardRadius = 18.0; - final cardElevation = 0.0; - final cardFont = width * 0.045; - final cardIcon = width * 0.11; - final buttonFont = width * 0.038; - final buttonPad = width * 0.03; return Scaffold( backgroundColor: greenBg, @@ -94,508 +287,203 @@ class _DashboardPageState extends State index: _selectedIndex, children: [ // Dashboard utama - Column( - children: [ - // Header - Stack( - children: [ - Container( - width: double.infinity, - height: headerHeight, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage('assets/greenhouse.jpg'), - fit: BoxFit.cover, - ), - borderRadius: const BorderRadius.only( - bottomLeft: Radius.circular(32), - bottomRight: Radius.circular(32), - ), - ), - child: Container( + SafeArea( + child: Column( + children: [ + // Header + Stack( + children: [ + Container( + width: double.infinity, + height: headerHeight, decoration: BoxDecoration( - gradient: LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [ - Colors.black.withOpacity(0.5), - Colors.black.withOpacity(0.3), - Colors.black.withOpacity(0.2), - ], - stops: [0.0, 0.5, 1.0], + image: DecorationImage( + image: AssetImage('assets/greenhouse.jpg'), + fit: BoxFit.cover, ), borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(32), bottomRight: Radius.circular(32), ), ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: EdgeInsets.only(left: 24, top: 24), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - padding: EdgeInsets.symmetric( - horizontal: 12, - vertical: 6, - ), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: Colors.white.withOpacity(0.2), - width: 1, + child: Container( + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [ + Colors.black.withOpacity(0.5), + Colors.black.withOpacity(0.3), + Colors.black.withOpacity(0.2), + ], + stops: [0.0, 0.5, 1.0], + ), + borderRadius: const BorderRadius.only( + bottomLeft: Radius.circular(32), + bottomRight: Radius.circular(32), + ), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.only(left: 20, top: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: EdgeInsets.symmetric( + horizontal: 10, + vertical: 4, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(20), + border: Border.all( + color: Colors.white.withOpacity(0.2), + width: 1, + ), + ), + child: Text( + 'Welcome', + style: TextStyle( + color: Colors.white, + fontWeight: FontWeight.w500, + fontSize: welcomeFont, + letterSpacing: 1.2, + ), ), ), - child: Text( - 'Welcome', - style: TextStyle( + SizedBox(height: 8), + Container( + width: logoSize, + height: logoSize, + decoration: BoxDecoration( color: Colors.white, - fontWeight: FontWeight.w500, - fontSize: welcomeFont * 1.1, - letterSpacing: 1.2, - shadows: [ - Shadow( - offset: Offset(1, 1), + borderRadius: BorderRadius.circular(16), + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), blurRadius: 8, - color: Colors.black.withOpacity(0.3), + offset: Offset(0, 2), ), ], ), - ), - ), - SizedBox(height: 12), - Container( - width: logoSize, - height: logoSize, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(20), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.15), - blurRadius: 12, - offset: Offset(0, 4), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(20), - child: BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 5, - sigmaY: 5, - ), - child: Padding( - padding: EdgeInsets.all(logoSize * 0.13), - child: Image.asset('assets/logo.png'), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: 5, + sigmaY: 5, + ), + child: Padding( + padding: EdgeInsets.all( + logoSize * 0.2, + ), + child: Image.asset('assets/logo.png'), + ), ), ), ), - ), - ], + ], + ), ), - ), - const Spacer(), - SizedBox(width: logoSize * 0.5), - ], - ), - ), - ), - // Jam di luar header card, rata kanan - Positioned( - right: 24, - top: headerHeight - (timeFont * 2.5), - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 8, - ), - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.15), - borderRadius: BorderRadius.circular(20), - border: Border.all( - color: Colors.white.withOpacity(0.2), - width: 1, + const Spacer(), + SizedBox(width: logoSize * 0.5), + ], ), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.1), - blurRadius: 8, - offset: Offset(0, 2), - ), - ], - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Icon( - Icons.access_time_rounded, - color: Colors.white, - size: 18, - ), - const SizedBox(width: 8), - Text( - time, - style: TextStyle( - color: Colors.white, - fontSize: timeFont, - fontWeight: FontWeight.w500, - letterSpacing: 1.1, - ), - ), - ], ), ), - ), - ], - ), - SizedBox(height: gridPadding), - // Grid 2x2 - Expanded( - child: Padding( - padding: EdgeInsets.symmetric(horizontal: gridPadding), - child: GridView.count( - crossAxisCount: 2, - mainAxisSpacing: gridSpacing, - crossAxisSpacing: gridSpacing, - childAspectRatio: 1, - physics: const NeverScrollableScrollPhysics(), - children: [ - // Temperature - LayoutBuilder( - builder: (context, constraints) { - final cardW = constraints.maxWidth; - final cardH = constraints.maxHeight; - final iconSize = cardW * 0.22; - final labelFont = cardW * 0.10; - final valueFont = cardW * 0.12; - final pad = cardH * 0.05; - return Container( - decoration: BoxDecoration( + // Jam di luar header card, rata kanan + Positioned( + right: 20, + top: headerHeight - (timeFont * 2.5), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 6, + ), + decoration: BoxDecoration( + color: Colors.white.withOpacity(0.15), + borderRadius: BorderRadius.circular(5), + border: Border.all( + color: Colors.white.withOpacity(0.2), + width: 1, + ), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + Icons.access_time_rounded, color: Colors.white, - borderRadius: BorderRadius.circular(cardRadius), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: Offset(0, 3), - ), - ], + size: 16, ), - child: ClipRRect( - borderRadius: BorderRadius.circular(cardRadius), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), - child: Padding( - padding: EdgeInsets.all(pad), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: EdgeInsets.all(iconSize * 0.18), - decoration: BoxDecoration( - color: blueTemp.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - Icons.thermostat_rounded, - color: blueTemp, - size: iconSize, - ), - ), - SizedBox(height: pad * 0.7), - Text( - 'Temperature', - style: TextStyle( - fontSize: labelFont, - fontWeight: FontWeight.w600, - color: blueTemp, - ), - ), - SizedBox(height: pad * 0.3), - Text( - '${temperature.toStringAsFixed(1)} °C', - style: TextStyle( - fontSize: valueFont, - fontWeight: FontWeight.bold, - color: blueTemp, - ), - ), - ], - ), - ), + const SizedBox(width: 6), + Text( + time, + style: TextStyle( + color: Colors.white, + fontSize: timeFont, + fontWeight: FontWeight.w500, ), ), - ); - }, + ], + ), ), - // Humidity - LayoutBuilder( - builder: (context, constraints) { - final cardW = constraints.maxWidth; - final cardH = constraints.maxHeight; - final iconSize = cardW * 0.22; - final labelFont = cardW * 0.10; - final valueFont = cardW * 0.12; - final pad = cardH * 0.05; - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(cardRadius), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: Offset(0, 3), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(cardRadius), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), - child: Padding( - padding: EdgeInsets.all(pad), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: EdgeInsets.all(iconSize * 0.18), - decoration: BoxDecoration( - color: blueHumidity.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - Icons.water_drop_rounded, - color: blueHumidity, - size: iconSize, - ), - ), - SizedBox(height: pad * 0.7), - Text( - 'Humidity', - style: TextStyle( - fontSize: labelFont, - fontWeight: FontWeight.w600, - color: blueHumidity, - ), - ), - SizedBox(height: pad * 0.3), - Text( - '${humidity.toStringAsFixed(1)} %', - style: TextStyle( - fontSize: valueFont, - fontWeight: FontWeight.bold, - color: blueHumidity, - ), - ), - ], - ), - ), - ), - ), - ); - }, - ), - // Kipas - LayoutBuilder( - builder: (context, constraints) { - final cardW = constraints.maxWidth; - final cardH = constraints.maxHeight; - final iconSize = cardW * 0.22; - final labelFont = cardW * 0.10; - final valueFont = cardW * 0.12; - final pad = cardH * 0.05; - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(cardRadius), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: Offset(0, 3), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(cardRadius), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), - child: Padding( - padding: EdgeInsets.all(pad), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: EdgeInsets.all(iconSize * 0.18), - decoration: BoxDecoration( - color: greenPrimary.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - Icons.ac_unit_rounded, - color: greenPrimary, - size: iconSize, - ), - ), - SizedBox(height: pad * 0.7), - Text( - 'Kipas', - style: TextStyle( - fontSize: labelFont, - fontWeight: FontWeight.w600, - color: greenPrimary, - ), - ), - SizedBox(height: pad * 0.5), - GestureDetector( - onTap: () { - setState(() { - fanOn = !fanOn; - _addHistory(); - }); - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: pad * 1.2, - vertical: pad * 0.7, - ), - decoration: BoxDecoration( - color: fanOn ? greenPrimary : Colors.grey[200], - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: (fanOn ? greenPrimary : Colors.grey[300]!).withOpacity(0.3), - blurRadius: 6, - offset: Offset(0, 2), - ), - ], - ), - child: Text( - fanOn ? 'ON' : 'OFF', - style: TextStyle( - color: fanOn ? Colors.white : Colors.grey[600], - fontSize: labelFont * 0.8, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ); - }, - ), - // Pompa - LayoutBuilder( - builder: (context, constraints) { - final cardW = constraints.maxWidth; - final cardH = constraints.maxHeight; - final iconSize = cardW * 0.22; - final labelFont = cardW * 0.10; - final valueFont = cardW * 0.12; - final pad = cardH * 0.05; - return Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(cardRadius), - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.05), - blurRadius: 10, - offset: Offset(0, 3), - ), - ], - ), - child: ClipRRect( - borderRadius: BorderRadius.circular(cardRadius), - child: BackdropFilter( - filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), - child: Padding( - padding: EdgeInsets.all(pad), - child: Column( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - padding: EdgeInsets.all(iconSize * 0.18), - decoration: BoxDecoration( - color: greenPrimary.withOpacity(0.1), - borderRadius: BorderRadius.circular(12), - ), - child: Icon( - Icons.water_rounded, - color: greenPrimary, - size: iconSize, - ), - ), - SizedBox(height: pad * 0.7), - Text( - 'Pompa', - style: TextStyle( - fontSize: labelFont, - fontWeight: FontWeight.w600, - color: greenPrimary, - ), - ), - SizedBox(height: pad * 0.5), - GestureDetector( - onTap: () { - setState(() { - pumpOn = !pumpOn; - _addHistory(); - }); - }, - child: Container( - padding: EdgeInsets.symmetric( - horizontal: pad * 1.2, - vertical: pad * 0.7, - ), - decoration: BoxDecoration( - color: pumpOn ? greenPrimary : Colors.grey[200], - borderRadius: BorderRadius.circular(16), - boxShadow: [ - BoxShadow( - color: (pumpOn ? greenPrimary : Colors.grey[300]!).withOpacity(0.3), - blurRadius: 6, - offset: Offset(0, 2), - ), - ], - ), - child: Text( - pumpOn ? 'ON' : 'OFF', - style: TextStyle( - color: pumpOn ? Colors.white : Colors.grey[600], - fontSize: labelFont * 0.8, - fontWeight: FontWeight.w600, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ); - }, - ), - ], + ), + ], + ), + SizedBox(height: gridPadding), + // Grid 2x2 + Expanded( + child: Padding( + padding: EdgeInsets.symmetric(horizontal: gridPadding), + child: GridView.count( + crossAxisCount: 2, + mainAxisSpacing: gridSpacing, + crossAxisSpacing: gridSpacing, + childAspectRatio: 1, + physics: const NeverScrollableScrollPhysics(), + children: [ + // Temperature + _buildSensorCard( + label: 'Temperature', + value: temperature, + unit: '°C', + icon: Icons.thermostat_rounded, + color: blueTemp, + ), + // Humidity + _buildSensorCard( + label: 'Humidity', + value: humidity, + unit: '%', + icon: Icons.water_drop_rounded, + color: blueHumidity, + ), + // Fan Control + _buildControlButton( + isOn: fanOn, + onTap: _toggleFan, + label: 'Kipas', + icon: Icons.ac_unit_rounded, + color: greenPrimary, + ), + // Pump Control + _buildControlButton( + isOn: pumpOn, + onTap: _togglePump, + label: 'Pompa', + icon: Icons.water_rounded, + color: greenPrimary, + ), + ], + ), ), ), - ), - ], + ], + ), ), // History page HistoryPage(history: history), diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..8b6a0dd --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,14 @@ +import 'package:firebase_core/firebase_core.dart'; + +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + return const FirebaseOptions( + apiKey: 'AIzaSyBB40eRLlP7kp52g2Hvmx3PkrkzYjuFnpo', + appId: '1:350710840656:android:d577c4c2427682b2408055', + messagingSenderId: '350710840656', + projectId: 'smartfarming-7bd96', + databaseURL: 'https://smartfarming-7bd96-default-rtdb.firebaseio.com', + storageBucket: 'smartfarming-7bd96.firebasestorage.app', + ); + } +} \ No newline at end of file diff --git a/lib/firebase_service.dart b/lib/firebase_service.dart new file mode 100644 index 0000000..46d045d --- /dev/null +++ b/lib/firebase_service.dart @@ -0,0 +1,78 @@ +import 'package:firebase_database/firebase_database.dart'; + +class FirebaseService { + final DatabaseReference _database = FirebaseDatabase.instance.ref(); + + // Stream untuk mendapatkan data sensor DHT11 + Stream> getSensorData() { + return _database.child('dht11').onValue.map((event) { + final data = event.snapshot.value as Map?; + if (data == null) return {}; + + return { + 'temperature': data['suhu']?.toDouble() ?? 0.0, + 'humidity': data['humidity']?.toDouble() ?? 0.0, + }; + }); + } + + // Stream untuk mendapatkan status kipas dari status/kipas + Stream getFanStatus() { + return _database.child('status/kipas').onValue.map((event) { + final data = event.snapshot.value as bool?; + return data ?? false; + }); + } + + // Stream untuk mendapatkan status pompa dari status/pompa + Stream getPumpStatus() { + return _database.child('status/pompa').onValue.map((event) { + final data = event.snapshot.value as bool?; + return data ?? false; + }); + } + + // Update status kipas langsung ke status/kipas dengan boolean + Future updateFanStatus(bool status) async { + await _database.child('status/kipas').set(status); + } + + // Update status pompa langsung ke status/pompa dengan boolean + Future updatePumpStatus(bool status) async { + await _database.child('status/pompa').set(status); + } + + // Stream untuk mendapatkan riwayat + Stream>> getHistory() { + return _database.child('riwayat').onValue.map((event) { + final data = event.snapshot.value as Map?; + if (data == null) return []; + + List> historyList = []; + data.forEach((key, value) { + if (value is Map) { + historyList.add({ + 'id': key, + 'temperature': value['temperature']?.toDouble() ?? 0.0, + 'humidity': value['humidity']?.toDouble() ?? 0.0, + 'fanOn': value['fanOn'] ?? false, + 'pumpOn': value['pumpOn'] ?? false, + 'timestamp': value['timestamp'] ?? 0, + }); + } + }); + + // Sort by timestamp descending (newest first) + historyList.sort((a, b) => (b['timestamp'] as int).compareTo(a['timestamp'] as int)); + return historyList; + }); + } + + // Tambah riwayat + Future addHistory(Map data) async { + await _database.child('riwayat').push().set({ + ...data, + 'timestamp': ServerValue.timestamp, + }); + } +} \ No newline at end of file diff --git a/lib/history_page.dart b/lib/history_page.dart index 3da0b1f..b586ec3 100644 --- a/lib/history_page.dart +++ b/lib/history_page.dart @@ -1,27 +1,22 @@ import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; -class HistoryEntry { - final DateTime time; - final double temperature; - final double humidity; - final bool fanOn; - final bool pumpOn; - HistoryEntry({ - required this.time, - required this.temperature, - required this.humidity, - required this.fanOn, - required this.pumpOn, - }); -} - -class HistoryPage extends StatelessWidget { - final List history; +class HistoryPage extends StatefulWidget { + final List> history; const HistoryPage({Key? key, required this.history}) : super(key: key); + @override + State createState() => _HistoryPageState(); +} + +class _HistoryPageState extends State { @override Widget build(BuildContext context) { final theme = Theme.of(context); + // Sort history by timestamp descending (newest first) + final sortedHistory = List>.from(widget.history) + ..sort((a, b) => (b['timestamp'] as int).compareTo(a['timestamp'] as int)); + return Scaffold( backgroundColor: const Color(0xFFF6FFF9), body: Column( @@ -31,7 +26,7 @@ class HistoryPage extends StatelessWidget { width: double.infinity, height: 110, decoration: BoxDecoration( - image: DecorationImage( + image: const DecorationImage( image: AssetImage('assets/greenhouse.jpg'), fit: BoxFit.cover, ), @@ -50,7 +45,7 @@ class HistoryPage extends StatelessWidget { Colors.black.withOpacity(0.3), Colors.black.withOpacity(0.2), ], - stops: [0.0, 0.5, 1.0], + stops: const [0.0, 0.5, 1.0], ), borderRadius: const BorderRadius.only( bottomLeft: Radius.circular(32), @@ -60,7 +55,7 @@ class HistoryPage extends StatelessWidget { child: Row( mainAxisAlignment: MainAxisAlignment.center, children: [ - Icon(Icons.history_rounded, color: Colors.white, size: 32), + const Icon(Icons.history_rounded, color: Colors.white, size: 32), const SizedBox(width: 12), Text( 'Riwayat Data', @@ -79,21 +74,21 @@ class HistoryPage extends StatelessWidget { Expanded( child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), - child: - history.isEmpty - ? Center( - child: Text( - 'Belum ada riwayat data', - style: TextStyle(fontSize: 16, color: Colors.grey), - ), - ) - : Card( - elevation: 6, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(22), - ), - child: Padding( - padding: const EdgeInsets.all(16), + child: widget.history.isEmpty + ? const Center( + child: Text( + 'Belum ada riwayat data', + style: TextStyle(fontSize: 16, color: Colors.grey), + ), + ) + : Card( + elevation: 6, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(22), + ), + child: Padding( + padding: const EdgeInsets.all(16), + child: SingleChildScrollView( child: SingleChildScrollView( scrollDirection: Axis.horizontal, child: DataTable( @@ -105,14 +100,14 @@ class HistoryPage extends StatelessWidget { color: Color(0xFF1CB56B), fontSize: 15, ), - dataRowColor: MaterialStateProperty.resolveWith< - Color? - >((Set states) { - if (states.contains(MaterialState.selected)) { - return Colors.green.withOpacity(0.15); - } - return null; - }), + dataRowColor: MaterialStateProperty.resolveWith( + (Set states) { + if (states.contains(MaterialState.selected)) { + return Colors.green.withOpacity(0.15); + } + return null; + }, + ), columns: const [ DataColumn(label: Text('Waktu')), DataColumn(label: Text('Temperature')), @@ -120,33 +115,35 @@ class HistoryPage extends StatelessWidget { DataColumn(label: Text('Kipas')), DataColumn(label: Text('Pompa')), ], - rows: List.generate(history.length, (i) { - final h = history[history.length - 1 - i]; + rows: List.generate(sortedHistory.length, (i) { + final h = sortedHistory[i]; + final timestamp = h['timestamp'] as int; + final date = DateTime.fromMillisecondsSinceEpoch(timestamp); + final formattedDate = DateFormat('dd/MM/yyyy HH:mm').format(date); final isEven = i % 2 == 0; + return DataRow( color: MaterialStateProperty.all( - isEven - ? Colors.white - : const Color(0xFFF1F8E9), + isEven ? Colors.white : const Color(0xFFF1F8E9), ), cells: [ DataCell( Text( - '${h.time.hour.toString().padLeft(2, '0')}:${h.time.minute.toString().padLeft(2, '0')}:${h.time.second.toString().padLeft(2, '0')}\n${h.time.day}/${h.time.month}/${h.time.year}', + formattedDate, style: const TextStyle(fontSize: 13), ), ), DataCell( Row( children: [ - Icon( + const Icon( Icons.thermostat, color: Color(0xFF4FC3F7), size: 18, ), const SizedBox(width: 4), Text( - '${h.temperature.toStringAsFixed(1)} °C', + '${(h['temperature'] as num).toStringAsFixed(1)} °C', style: const TextStyle( fontSize: 13, ), @@ -157,14 +154,14 @@ class HistoryPage extends StatelessWidget { DataCell( Row( children: [ - Icon( + const Icon( Icons.water_drop, color: Color(0xFF0288D1), size: 18, ), const SizedBox(width: 4), Text( - '${h.humidity.toStringAsFixed(1)} %', + '${(h['humidity'] as num).toStringAsFixed(1)} %', style: const TextStyle( fontSize: 13, ), @@ -179,23 +176,19 @@ class HistoryPage extends StatelessWidget { vertical: 4, ), decoration: BoxDecoration( - color: - h.fanOn - ? const Color(0xFFB9F6CA) - : const Color(0xFFFFCDD2), - borderRadius: BorderRadius.circular( - 12, - ), + color: h['fanOn'] == true + ? const Color(0xFFB9F6CA) + : const Color(0xFFFFCDD2), + borderRadius: BorderRadius.circular(12), ), child: Text( - h.fanOn ? 'ON' : 'OFF', + h['fanOn'] == true ? 'ON' : 'OFF', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, - color: - h.fanOn - ? Colors.green[800] - : Colors.red[800], + color: h['fanOn'] == true + ? Colors.green[800] + : Colors.red[800], ), ), ), @@ -207,23 +200,19 @@ class HistoryPage extends StatelessWidget { vertical: 4, ), decoration: BoxDecoration( - color: - h.pumpOn - ? const Color(0xFFB9F6CA) - : const Color(0xFFFFCDD2), - borderRadius: BorderRadius.circular( - 12, - ), + color: h['pumpOn'] == true + ? const Color(0xFFB9F6CA) + : const Color(0xFFFFCDD2), + borderRadius: BorderRadius.circular(12), ), child: Text( - h.pumpOn ? 'ON' : 'OFF', + h['pumpOn'] == true ? 'ON' : 'OFF', style: TextStyle( fontSize: 13, fontWeight: FontWeight.bold, - color: - h.pumpOn - ? Colors.green[800] - : Colors.red[800], + color: h['pumpOn'] == true + ? Colors.green[800] + : Colors.red[800], ), ), ), @@ -235,6 +224,7 @@ class HistoryPage extends StatelessWidget { ), ), ), + ), ), ), ], diff --git a/lib/main.dart b/lib/main.dart index 65f6296..3499742 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,8 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:firebase_core/firebase_core.dart'; import 'splash_screen.dart'; import 'dashboard.dart'; +import 'firebase_options.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, + ); runApp(const MyApp()); } @@ -13,16 +19,17 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', - theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - ), - debugShowCheckedModeBanner: false, - initialRoute: '/', - routes: { - '/': (context) => const SplashScreen(), - '/home': (context) => const DashboardPage(), - }, - ); + title: 'Smart Farm', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.green), + useMaterial3: true, + ), + debugShowCheckedModeBanner: false, + initialRoute: '/', + routes: { + '/': (context) => const SplashScreen(), + '/home': (context) => const DashboardPage(), + }, + ); } } diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b..4b81f9b 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b..5caa9d1 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index cccf817..038a4bf 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,6 +5,12 @@ import FlutterMacOS import Foundation +import cloud_firestore +import firebase_core +import firebase_database func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin")) } diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 0000000..29c8eb3 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '10.14' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/pubspec.lock b/pubspec.lock index c2c57f7..f757ff6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7" + url: "https://pub.dev" + source: hosted + version: "1.3.35" async: dependency: transitive description: @@ -33,6 +41,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.2" + cloud_firestore: + dependency: "direct main" + description: + name: cloud_firestore + sha256: a0f161b92610e078b4962d7e6ebeb66dc9cce0ada3514aeee442f68165d78185 + url: "https://pub.dev" + source: hosted + version: "4.17.5" + cloud_firestore_platform_interface: + dependency: transitive + description: + name: cloud_firestore_platform_interface + sha256: "6a55b319f8d33c307396b9104512e8130a61904528ab7bd8b5402678fca54b81" + url: "https://pub.dev" + source: hosted + version: "6.2.5" + cloud_firestore_web: + dependency: transitive + description: + name: cloud_firestore_web + sha256: "89dfa1304d3da48b3039abbb2865e3d30896ef858e569a16804a99f4362283a9" + url: "https://pub.dev" + source: hosted + version: "3.12.5" collection: dependency: transitive description: @@ -57,6 +89,54 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.2" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c" + url: "https://pub.dev" + source: hosted + version: "2.32.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf + url: "https://pub.dev" + source: hosted + version: "5.4.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: ddd72baa6f727e5b23f32d9af23d7d453d67946f380bd9c21daf474ee0f7326e + url: "https://pub.dev" + source: hosted + version: "2.23.0" + firebase_database: + dependency: "direct main" + description: + name: firebase_database + sha256: "3b9ca306d26ad243ccbc4c717ff6e8563a080ebe11ee77fa7349b419c894b42d" + url: "https://pub.dev" + source: hosted + version: "10.5.7" + firebase_database_platform_interface: + dependency: transitive + description: + name: firebase_database_platform_interface + sha256: "5864cc362275465e9bd682b243f19419c9d78b861c2db820241eea596ae3b320" + url: "https://pub.dev" + source: hosted + version: "0.2.5+35" + firebase_database_web: + dependency: transitive + description: + name: firebase_database_web + sha256: a6008395dd20e8b8dde0691b441c181a1216c3866f89f48dcb6889d34fd35905 + url: "https://pub.dev" + source: hosted + version: "0.2.5+7" flutter: dependency: "direct main" description: flutter @@ -75,6 +155,19 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + intl: + dependency: "direct main" + description: + name: intl + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" + url: "https://pub.dev" + source: hosted + version: "0.20.2" leak_tracker: dependency: transitive description: @@ -139,6 +232,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" sky_engine: dependency: transitive description: flutter @@ -208,6 +309,14 @@ packages: url: "https://pub.dev" source: hosted version: "14.3.1" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" sdks: dart: ">=3.7.0 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index ede45d5..83db2ea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -34,6 +34,12 @@ dependencies: # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. cupertino_icons: ^1.0.8 + + # Firebase + firebase_core: ^2.24.2 + firebase_database: ^10.3.8 + cloud_firestore: ^4.14.0 + intl: ^0.20.2 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..eeeeb11 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,12 @@ #include "generated_plugin_registrant.h" +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + CloudFirestorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("CloudFirestorePluginCApi")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..448a2c3 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,8 @@ # list(APPEND FLUTTER_PLUGIN_LIST + cloud_firestore + firebase_core ) list(APPEND FLUTTER_FFI_PLUGIN_LIST