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"

View File

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.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 'package:flutter/material.dart';
import 'firebase_service.dart';
import 'history_page.dart';
class DashboardPage extends StatefulWidget {
@ -19,8 +21,9 @@ class _DashboardPageState extends State<DashboardPage>
int _selectedIndex = 0;
late AnimationController _controller;
late Animation<double> _fadeAnim;
final FirebaseService _firebaseService = FirebaseService();
List<HistoryEntry> history = [];
List<Map<String, dynamic>> history = [];
@override
void initState() {
@ -34,6 +37,43 @@ class _DashboardPageState extends State<DashboardPage>
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<DashboardPage>
}
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<DashboardPage>
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<DashboardPage>
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),

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: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<HistoryEntry> history;
class HistoryPage extends StatefulWidget {
final List<Map<String, dynamic>> history;
const HistoryPage({Key? key, required this.history}) : super(key: key);
@override
State<HistoryPage> createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> {
@override
Widget build(BuildContext 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(
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<MaterialState> states) {
if (states.contains(MaterialState.selected)) {
return Colors.green.withOpacity(0.15);
}
return null;
}),
dataRowColor: MaterialStateProperty.resolveWith<Color?>(
(Set<MaterialState> 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 {
),
),
),
),
),
),
],

View File

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

View File

@ -1 +1,2 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.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"

View File

@ -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"))
}

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
# 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"

View File

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

View File

@ -6,6 +6,12 @@
#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) {
CloudFirestorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("CloudFirestorePluginCApi"));
FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
}

View File

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