This commit is contained in:
aarsyah0 2025-06-18 15:20:58 +07:00
parent 6ded76c325
commit 0efd3de196
16 changed files with 777 additions and 582 deletions

View File

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"

View File

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"

43
ios/Podfile Normal file
View File

@ -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

View File

@ -1,6 +1,8 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'firebase_service.dart';
import 'history_page.dart'; import 'history_page.dart';
class DashboardPage extends StatefulWidget { class DashboardPage extends StatefulWidget {
@ -19,8 +21,9 @@ class _DashboardPageState extends State<DashboardPage>
int _selectedIndex = 0; int _selectedIndex = 0;
late AnimationController _controller; late AnimationController _controller;
late Animation<double> _fadeAnim; late Animation<double> _fadeAnim;
final FirebaseService _firebaseService = FirebaseService();
List<HistoryEntry> history = []; List<Map<String, dynamic>> history = [];
@override @override
void initState() { void initState() {
@ -34,6 +37,43 @@ class _DashboardPageState extends State<DashboardPage>
curve: Curves.easeOutCubic, curve: Curves.easeOutCubic,
); );
_controller.forward(); _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 @override
@ -51,19 +91,178 @@ class _DashboardPageState extends State<DashboardPage>
} }
void _addHistory() { void _addHistory() {
history.add(HistoryEntry( final historyData = {
time: DateTime.now(), 'temperature': temperature,
temperature: temperature, 'humidity': humidity,
humidity: humidity, 'fanOn': fanOn,
fanOn: fanOn, 'pumpOn': pumpOn,
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) { Widget _buildDashboard(BuildContext context) {
final size = MediaQuery.of(context).size; final size = MediaQuery.of(context).size;
final width = size.width; final width = size.width;
final height = size.height; final height = size.height;
// Palet warna // Palet warna
const greenPrimary = Color(0xFF1CB56B); const greenPrimary = Color(0xFF1CB56B);
const greenGradientStart = Color(0xFF43EA7A); const greenGradientStart = Color(0xFF43EA7A);
@ -73,20 +272,14 @@ class _DashboardPageState extends State<DashboardPage>
final time = TimeOfDay.now().format(context); final time = TimeOfDay.now().format(context);
// Header // Header
final headerHeight = height * 0.17; final headerHeight = height * 0.15;
final logoSize = width * 0.10; final logoSize = width * 0.08;
final welcomeFont = width * 0.045; final welcomeFont = width * 0.04;
final timeFont = width * 0.032; final timeFont = width * 0.03;
// Grid // Grid
final gridPadding = width * 0.04; final gridPadding = width * 0.04;
final gridSpacing = width * 0.03; 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( return Scaffold(
backgroundColor: greenBg, backgroundColor: greenBg,
@ -94,508 +287,203 @@ class _DashboardPageState extends State<DashboardPage>
index: _selectedIndex, index: _selectedIndex,
children: [ children: [
// Dashboard utama // Dashboard utama
Column( SafeArea(
children: [ child: Column(
// Header children: [
Stack( // Header
children: [ Stack(
Container( children: [
width: double.infinity, Container(
height: headerHeight, width: double.infinity,
decoration: BoxDecoration( height: headerHeight,
image: DecorationImage(
image: AssetImage('assets/greenhouse.jpg'),
fit: BoxFit.cover,
),
borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(32),
bottomRight: Radius.circular(32),
),
),
child: Container(
decoration: BoxDecoration( decoration: BoxDecoration(
gradient: LinearGradient( image: DecorationImage(
begin: Alignment.topLeft, image: AssetImage('assets/greenhouse.jpg'),
end: Alignment.bottomRight, fit: BoxFit.cover,
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( borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(32), bottomLeft: Radius.circular(32),
bottomRight: Radius.circular(32), bottomRight: Radius.circular(32),
), ),
), ),
child: Row( child: Container(
crossAxisAlignment: CrossAxisAlignment.start, decoration: BoxDecoration(
children: [ gradient: LinearGradient(
Padding( begin: Alignment.topLeft,
padding: EdgeInsets.only(left: 24, top: 24), end: Alignment.bottomRight,
child: Column( colors: [
crossAxisAlignment: CrossAxisAlignment.start, Colors.black.withOpacity(0.5),
children: [ Colors.black.withOpacity(0.3),
Container( Colors.black.withOpacity(0.2),
padding: EdgeInsets.symmetric( ],
horizontal: 12, stops: [0.0, 0.5, 1.0],
vertical: 6, ),
), borderRadius: const BorderRadius.only(
decoration: BoxDecoration( bottomLeft: Radius.circular(32),
color: Colors.white.withOpacity(0.15), bottomRight: Radius.circular(32),
borderRadius: BorderRadius.circular(20), ),
border: Border.all( ),
color: Colors.white.withOpacity(0.2), child: Row(
width: 1, 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( SizedBox(height: 8),
'Welcome', Container(
style: TextStyle( width: logoSize,
height: logoSize,
decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
fontWeight: FontWeight.w500, borderRadius: BorderRadius.circular(16),
fontSize: welcomeFont * 1.1, boxShadow: [
letterSpacing: 1.2, BoxShadow(
shadows: [ color: Colors.black.withOpacity(0.15),
Shadow(
offset: Offset(1, 1),
blurRadius: 8, blurRadius: 8,
color: Colors.black.withOpacity(0.3), offset: Offset(0, 2),
), ),
], ],
), ),
), child: ClipRRect(
), borderRadius: BorderRadius.circular(16),
SizedBox(height: 12), child: BackdropFilter(
Container( filter: ImageFilter.blur(
width: logoSize, sigmaX: 5,
height: logoSize, sigmaY: 5,
decoration: BoxDecoration( ),
color: Colors.white, child: Padding(
borderRadius: BorderRadius.circular(20), padding: EdgeInsets.all(
boxShadow: [ logoSize * 0.2,
BoxShadow( ),
color: Colors.black.withOpacity(0.15), child: Image.asset('assets/logo.png'),
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'),
), ),
), ),
), ),
), ],
], ),
), ),
), const Spacer(),
const Spacer(), SizedBox(width: logoSize * 0.5),
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,
), ),
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,
),
),
],
), ),
), ),
), // Jam di luar header card, rata kanan
], Positioned(
), right: 20,
SizedBox(height: gridPadding), top: headerHeight - (timeFont * 2.5),
// Grid 2x2 child: Container(
Expanded( padding: const EdgeInsets.symmetric(
child: Padding( horizontal: 12,
padding: EdgeInsets.symmetric(horizontal: gridPadding), vertical: 6,
child: GridView.count( ),
crossAxisCount: 2, decoration: BoxDecoration(
mainAxisSpacing: gridSpacing, color: Colors.white.withOpacity(0.15),
crossAxisSpacing: gridSpacing, borderRadius: BorderRadius.circular(5),
childAspectRatio: 1, border: Border.all(
physics: const NeverScrollableScrollPhysics(), color: Colors.white.withOpacity(0.2),
children: [ width: 1,
// Temperature ),
LayoutBuilder( ),
builder: (context, constraints) { child: Row(
final cardW = constraints.maxWidth; mainAxisSize: MainAxisSize.min,
final cardH = constraints.maxHeight; children: [
final iconSize = cardW * 0.22; Icon(
final labelFont = cardW * 0.10; Icons.access_time_rounded,
final valueFont = cardW * 0.12;
final pad = cardH * 0.05;
return Container(
decoration: BoxDecoration(
color: Colors.white, color: Colors.white,
borderRadius: BorderRadius.circular(cardRadius), size: 16,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
offset: Offset(0, 3),
),
],
), ),
child: ClipRRect( const SizedBox(width: 6),
borderRadius: BorderRadius.circular(cardRadius), Text(
child: BackdropFilter( time,
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), style: TextStyle(
child: Padding( color: Colors.white,
padding: EdgeInsets.all(pad), fontSize: timeFont,
child: Column( fontWeight: FontWeight.w500,
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,
),
),
],
),
),
), ),
), ),
); ],
}, ),
), ),
// Humidity ),
LayoutBuilder( ],
builder: (context, constraints) { ),
final cardW = constraints.maxWidth; SizedBox(height: gridPadding),
final cardH = constraints.maxHeight; // Grid 2x2
final iconSize = cardW * 0.22; Expanded(
final labelFont = cardW * 0.10; child: Padding(
final valueFont = cardW * 0.12; padding: EdgeInsets.symmetric(horizontal: gridPadding),
final pad = cardH * 0.05; child: GridView.count(
return Container( crossAxisCount: 2,
decoration: BoxDecoration( mainAxisSpacing: gridSpacing,
color: Colors.white, crossAxisSpacing: gridSpacing,
borderRadius: BorderRadius.circular(cardRadius), childAspectRatio: 1,
boxShadow: [ physics: const NeverScrollableScrollPhysics(),
BoxShadow( children: [
color: Colors.black.withOpacity(0.05), // Temperature
blurRadius: 10, _buildSensorCard(
offset: Offset(0, 3), label: 'Temperature',
), value: temperature,
], unit: '°C',
), icon: Icons.thermostat_rounded,
child: ClipRRect( color: blueTemp,
borderRadius: BorderRadius.circular(cardRadius), ),
child: BackdropFilter( // Humidity
filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), _buildSensorCard(
child: Padding( label: 'Humidity',
padding: EdgeInsets.all(pad), value: humidity,
child: Column( unit: '%',
mainAxisSize: MainAxisSize.min, icon: Icons.water_drop_rounded,
mainAxisAlignment: MainAxisAlignment.center, color: blueHumidity,
children: [ ),
Container( // Fan Control
padding: EdgeInsets.all(iconSize * 0.18), _buildControlButton(
decoration: BoxDecoration( isOn: fanOn,
color: blueHumidity.withOpacity(0.1), onTap: _toggleFan,
borderRadius: BorderRadius.circular(12), label: 'Kipas',
), icon: Icons.ac_unit_rounded,
child: Icon( color: greenPrimary,
Icons.water_drop_rounded, ),
color: blueHumidity, // Pump Control
size: iconSize, _buildControlButton(
), isOn: pumpOn,
), onTap: _togglePump,
SizedBox(height: pad * 0.7), label: 'Pompa',
Text( icon: Icons.water_rounded,
'Humidity', color: greenPrimary,
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,
),
),
),
),
],
),
),
),
),
);
},
),
],
), ),
), ),
), ],
], ),
), ),
// History page // History page
HistoryPage(history: history), HistoryPage(history: history),

14
lib/firebase_options.dart Normal file
View File

@ -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',
);
}
}

78
lib/firebase_service.dart Normal file
View File

@ -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<Map<String, dynamic>> getSensorData() {
return _database.child('dht11').onValue.map((event) {
final data = event.snapshot.value as Map<dynamic, dynamic>?;
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<bool> 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<bool> 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<void> updateFanStatus(bool status) async {
await _database.child('status/kipas').set(status);
}
// Update status pompa langsung ke status/pompa dengan boolean
Future<void> updatePumpStatus(bool status) async {
await _database.child('status/pompa').set(status);
}
// Stream untuk mendapatkan riwayat
Stream<List<Map<String, dynamic>>> getHistory() {
return _database.child('riwayat').onValue.map((event) {
final data = event.snapshot.value as Map<dynamic, dynamic>?;
if (data == null) return [];
List<Map<String, dynamic>> 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<void> addHistory(Map<String, dynamic> data) async {
await _database.child('riwayat').push().set({
...data,
'timestamp': ServerValue.timestamp,
});
}
}

View File

@ -1,27 +1,22 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class HistoryEntry { class HistoryPage extends StatefulWidget {
final DateTime time; final List<Map<String, dynamic>> history;
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<HistoryEntry> history;
const HistoryPage({Key? key, required this.history}) : super(key: key); const HistoryPage({Key? key, required this.history}) : super(key: key);
@override
State<HistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final theme = Theme.of(context); final theme = Theme.of(context);
// Sort history by timestamp descending (newest first)
final sortedHistory = List<Map<String, dynamic>>.from(widget.history)
..sort((a, b) => (b['timestamp'] as int).compareTo(a['timestamp'] as int));
return Scaffold( return Scaffold(
backgroundColor: const Color(0xFFF6FFF9), backgroundColor: const Color(0xFFF6FFF9),
body: Column( body: Column(
@ -31,7 +26,7 @@ class HistoryPage extends StatelessWidget {
width: double.infinity, width: double.infinity,
height: 110, height: 110,
decoration: BoxDecoration( decoration: BoxDecoration(
image: DecorationImage( image: const DecorationImage(
image: AssetImage('assets/greenhouse.jpg'), image: AssetImage('assets/greenhouse.jpg'),
fit: BoxFit.cover, fit: BoxFit.cover,
), ),
@ -50,7 +45,7 @@ class HistoryPage extends StatelessWidget {
Colors.black.withOpacity(0.3), Colors.black.withOpacity(0.3),
Colors.black.withOpacity(0.2), Colors.black.withOpacity(0.2),
], ],
stops: [0.0, 0.5, 1.0], stops: const [0.0, 0.5, 1.0],
), ),
borderRadius: const BorderRadius.only( borderRadius: const BorderRadius.only(
bottomLeft: Radius.circular(32), bottomLeft: Radius.circular(32),
@ -60,7 +55,7 @@ class HistoryPage extends StatelessWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
Icon(Icons.history_rounded, color: Colors.white, size: 32), const Icon(Icons.history_rounded, color: Colors.white, size: 32),
const SizedBox(width: 12), const SizedBox(width: 12),
Text( Text(
'Riwayat Data', 'Riwayat Data',
@ -79,21 +74,21 @@ class HistoryPage extends StatelessWidget {
Expanded( Expanded(
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: child: widget.history.isEmpty
history.isEmpty ? const Center(
? Center( child: Text(
child: Text( 'Belum ada riwayat data',
'Belum ada riwayat data', style: TextStyle(fontSize: 16, color: Colors.grey),
style: TextStyle(fontSize: 16, color: Colors.grey), ),
), )
) : Card(
: Card( elevation: 6,
elevation: 6, shape: RoundedRectangleBorder(
shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(22),
borderRadius: BorderRadius.circular(22), ),
), child: Padding(
child: Padding( padding: const EdgeInsets.all(16),
padding: const EdgeInsets.all(16), child: SingleChildScrollView(
child: SingleChildScrollView( child: SingleChildScrollView(
scrollDirection: Axis.horizontal, scrollDirection: Axis.horizontal,
child: DataTable( child: DataTable(
@ -105,14 +100,14 @@ class HistoryPage extends StatelessWidget {
color: Color(0xFF1CB56B), color: Color(0xFF1CB56B),
fontSize: 15, fontSize: 15,
), ),
dataRowColor: MaterialStateProperty.resolveWith< dataRowColor: MaterialStateProperty.resolveWith<Color?>(
Color? (Set<MaterialState> states) {
>((Set<MaterialState> states) { if (states.contains(MaterialState.selected)) {
if (states.contains(MaterialState.selected)) { return Colors.green.withOpacity(0.15);
return Colors.green.withOpacity(0.15); }
} return null;
return null; },
}), ),
columns: const [ columns: const [
DataColumn(label: Text('Waktu')), DataColumn(label: Text('Waktu')),
DataColumn(label: Text('Temperature')), DataColumn(label: Text('Temperature')),
@ -120,33 +115,35 @@ class HistoryPage extends StatelessWidget {
DataColumn(label: Text('Kipas')), DataColumn(label: Text('Kipas')),
DataColumn(label: Text('Pompa')), DataColumn(label: Text('Pompa')),
], ],
rows: List.generate(history.length, (i) { rows: List.generate(sortedHistory.length, (i) {
final h = history[history.length - 1 - 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; final isEven = i % 2 == 0;
return DataRow( return DataRow(
color: MaterialStateProperty.all( color: MaterialStateProperty.all(
isEven isEven ? Colors.white : const Color(0xFFF1F8E9),
? Colors.white
: const Color(0xFFF1F8E9),
), ),
cells: [ cells: [
DataCell( DataCell(
Text( 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), style: const TextStyle(fontSize: 13),
), ),
), ),
DataCell( DataCell(
Row( Row(
children: [ children: [
Icon( const Icon(
Icons.thermostat, Icons.thermostat,
color: Color(0xFF4FC3F7), color: Color(0xFF4FC3F7),
size: 18, size: 18,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'${h.temperature.toStringAsFixed(1)} °C', '${(h['temperature'] as num).toStringAsFixed(1)} °C',
style: const TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 13,
), ),
@ -157,14 +154,14 @@ class HistoryPage extends StatelessWidget {
DataCell( DataCell(
Row( Row(
children: [ children: [
Icon( const Icon(
Icons.water_drop, Icons.water_drop,
color: Color(0xFF0288D1), color: Color(0xFF0288D1),
size: 18, size: 18,
), ),
const SizedBox(width: 4), const SizedBox(width: 4),
Text( Text(
'${h.humidity.toStringAsFixed(1)} %', '${(h['humidity'] as num).toStringAsFixed(1)} %',
style: const TextStyle( style: const TextStyle(
fontSize: 13, fontSize: 13,
), ),
@ -179,23 +176,19 @@ class HistoryPage extends StatelessWidget {
vertical: 4, vertical: 4,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: h['fanOn'] == true
h.fanOn ? const Color(0xFFB9F6CA)
? const Color(0xFFB9F6CA) : const Color(0xFFFFCDD2),
: const Color(0xFFFFCDD2), borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(
12,
),
), ),
child: Text( child: Text(
h.fanOn ? 'ON' : 'OFF', h['fanOn'] == true ? 'ON' : 'OFF',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: color: h['fanOn'] == true
h.fanOn ? Colors.green[800]
? Colors.green[800] : Colors.red[800],
: Colors.red[800],
), ),
), ),
), ),
@ -207,23 +200,19 @@ class HistoryPage extends StatelessWidget {
vertical: 4, vertical: 4,
), ),
decoration: BoxDecoration( decoration: BoxDecoration(
color: color: h['pumpOn'] == true
h.pumpOn ? const Color(0xFFB9F6CA)
? const Color(0xFFB9F6CA) : const Color(0xFFFFCDD2),
: const Color(0xFFFFCDD2), borderRadius: BorderRadius.circular(12),
borderRadius: BorderRadius.circular(
12,
),
), ),
child: Text( child: Text(
h.pumpOn ? 'ON' : 'OFF', h['pumpOn'] == true ? 'ON' : 'OFF',
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: color: h['pumpOn'] == true
h.pumpOn ? Colors.green[800]
? Colors.green[800] : Colors.red[800],
: Colors.red[800],
), ),
), ),
), ),
@ -235,6 +224,7 @@ class HistoryPage extends StatelessWidget {
), ),
), ),
), ),
),
), ),
), ),
], ],

View File

@ -1,8 +1,14 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:firebase_core/firebase_core.dart';
import 'splash_screen.dart'; import 'splash_screen.dart';
import 'dashboard.dart'; import 'dashboard.dart';
import 'firebase_options.dart';
void main() { void main() async {
WidgetsFlutterBinding.ensureInitialized();
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform,
);
runApp(const MyApp()); runApp(const MyApp());
} }
@ -13,16 +19,17 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
title: 'Flutter Demo', title: 'Smart Farm',
theme: ThemeData( theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), colorScheme: ColorScheme.fromSeed(seedColor: Colors.green),
), useMaterial3: true,
debugShowCheckedModeBanner: false, ),
initialRoute: '/', debugShowCheckedModeBanner: false,
routes: { initialRoute: '/',
'/': (context) => const SplashScreen(), routes: {
'/home': (context) => const DashboardPage(), '/': (context) => const SplashScreen(),
}, '/home': (context) => const DashboardPage(),
); },
);
} }
} }

View File

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig"

View File

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "ephemeral/Flutter-Generated.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig"

View File

@ -5,6 +5,12 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import cloud_firestore
import firebase_core
import firebase_database
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 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"))
} }

42
macos/Podfile Normal file
View File

@ -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

View File

@ -1,6 +1,14 @@
# Generated by pub # Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile # See https://dart.dev/tools/pub/glossary#lockfile
packages: packages:
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7"
url: "https://pub.dev"
source: hosted
version: "1.3.35"
async: async:
dependency: transitive dependency: transitive
description: description:
@ -33,6 +41,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.2" 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: collection:
dependency: transitive dependency: transitive
description: description:
@ -57,6 +89,54 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.2" 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: flutter:
dependency: "direct main" dependency: "direct main"
description: flutter description: flutter
@ -75,6 +155,19 @@ packages:
description: flutter description: flutter
source: sdk source: sdk
version: "0.0.0" 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: leak_tracker:
dependency: transitive dependency: transitive
description: description:
@ -139,6 +232,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.9.1" 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: sky_engine:
dependency: transitive dependency: transitive
description: flutter description: flutter
@ -208,6 +309,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "14.3.1" version: "14.3.1"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
sdks: sdks:
dart: ">=3.7.0 <4.0.0" dart: ">=3.7.0 <4.0.0"
flutter: ">=3.18.0-18.0.pre.54" flutter: ">=3.22.0"

View File

@ -34,6 +34,12 @@ dependencies:
# The following adds the Cupertino Icons font to your application. # The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons. # Use with the CupertinoIcons class for iOS style icons.
cupertino_icons: ^1.0.8 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: dev_dependencies:
flutter_test: flutter_test:

View File

@ -6,6 +6,12 @@
#include "generated_plugin_registrant.h" #include "generated_plugin_registrant.h"
#include <cloud_firestore/cloud_firestore_plugin_c_api.h>
#include <firebase_core/firebase_core_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) { void RegisterPlugins(flutter::PluginRegistry* registry) {
CloudFirestorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("CloudFirestorePluginCApi"));
FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
} }

View File

@ -3,6 +3,8 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
cloud_firestore
firebase_core
) )
list(APPEND FLUTTER_FFI_PLUGIN_LIST list(APPEND FLUTTER_FFI_PLUGIN_LIST