This commit is contained in:
kleponijo 2026-04-12 13:55:12 +07:00
parent 5d7d61fbaa
commit 3675c0188d
34 changed files with 1630 additions and 203 deletions

View File

@ -2,19 +2,19 @@ import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:klimatologiot/app_view.dart';
import 'package:user_repository/user_repository.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
import 'blocs/authentication_bloc/authentication_bloc.dart';
class MyApp extends StatelessWidget {
final UserRepository userRepository;
const MyApp(this.userRepository, {super.key});
final MonitoringRepository monitoringRepository;
const MyApp(this.userRepository, this.monitoringRepository, {super.key});
@override
Widget build(BuildContext context) {
return RepositoryProvider<AuthenticationBloc>(
create: (context) => AuthenticationBloc(
userRepository: userRepository
),
child: const MyAppView(),
create: (context) => AuthenticationBloc(userRepository: userRepository),
child: const MyAppView(),
);
}
}
}

View File

@ -0,0 +1,50 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:firebase_database/firebase_database.dart';
part 'monitoring_event.dart';
part 'monitoring_state.dart';
class MonitoringBloc extends Bloc<MonitoringEvent, MonitoringState> {
final DatabaseReference _realtimeRef =
FirebaseDatabase.instance.ref('anemometer/realtime');
StreamSubscription<DatabaseEvent>? _realtimeSub;
MonitoringBloc() : super(const MonitoringState.initial()) {
on<LoadMonitoringData>(_onLoadMonitoringData);
on<MonitoringDataUpdated>(_onMonitoringDataUpdated);
}
void _onLoadMonitoringData(
LoadMonitoringData event, Emitter<MonitoringState> emit) {
emit(const MonitoringState.loading());
// Start listening to realtime data
_realtimeSub = _realtimeRef.onValue.listen((event) {
final data = event.snapshot.value;
if (data is Map) {
final speed = (data['kecepatan'] ?? 0).toDouble();
final timestamp = data['timestamp'] ?? 0;
final waktu = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
add(MonitoringDataUpdated(speed: speed, timestamp: waktu));
}
});
}
void _onMonitoringDataUpdated(
MonitoringDataUpdated event, Emitter<MonitoringState> emit) {
emit(MonitoringState.loaded(
currentSpeed: event.speed,
lastUpdateTime: event.timestamp,
));
}
@override
Future<void> close() {
_realtimeSub?.cancel();
return super.close();
}
}

View File

@ -0,0 +1,25 @@
part of 'monitoring_bloc.dart';
sealed class MonitoringEvent extends Equatable {
const MonitoringEvent();
@override
List<Object> get props => [];
}
class LoadMonitoringData extends MonitoringEvent {
const LoadMonitoringData();
}
class MonitoringDataUpdated extends MonitoringEvent {
final double speed;
final DateTime timestamp;
const MonitoringDataUpdated({
required this.speed,
required this.timestamp,
});
@override
List<Object> get props => [speed, timestamp];
}

View File

@ -0,0 +1,34 @@
part of 'monitoring_bloc.dart';
class MonitoringState extends Equatable {
final MonitoringStatus status;
final double? currentSpeed;
final DateTime? lastUpdateTime;
const MonitoringState._({
this.status = MonitoringStatus.initial,
this.currentSpeed,
this.lastUpdateTime,
});
const MonitoringState.initial() : this._();
const MonitoringState.loading() : this._(status: MonitoringStatus.loading);
const MonitoringState.loaded({
required double currentSpeed,
required DateTime lastUpdateTime,
}) : this._(
status: MonitoringStatus.loaded,
currentSpeed: currentSpeed,
lastUpdateTime: lastUpdateTime,
);
const MonitoringState.error(String message)
: this._(status: MonitoringStatus.error);
@override
List<Object?> get props => [status, currentSpeed, lastUpdateTime];
}
enum MonitoringStatus { initial, loading, loaded, error }

View File

@ -4,6 +4,7 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:klimatologiot/simple_bloc_observer.dart';
import 'package:klimatologiot/app.dart';
import 'package:user_repository/user_repository.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
import 'firebase_options.dart';
void main() async {
@ -12,5 +13,8 @@ void main() async {
options: DefaultFirebaseOptions.currentPlatform,
);
Bloc.observer = SimpleBlocObserver();
runApp(MyApp(FirebaseUserRepo()));
}
runApp(MyApp(
FirebaseUserRepo(),
FirebaseMonitoringRepo(),
));
}

View File

View File

View File

View File

@ -5,7 +5,7 @@ import 'package:firebase_database/firebase_database.dart';
import 'dart:async';
import '../../../blocs/authentication_bloc/authentication_bloc.dart';
import '../../monitoring/evaporasi_monitoring_screen.dart';
import '../../monitoring/wind_speed/wind_speed_monitoring_screen.dart';
import '../../monitoring/wind_speed/wind_speed.dart';
import '../../monitoring/air_quality_monitoring_screen.dart';
class HomeScreen extends StatefulWidget {

View File

@ -1,16 +1,18 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import 'widgets/monitoring_shared.dart';
import 'shared/widgets/monitoring_shared.dart';
/// AIR QUALITY MONITORING SCREEN
class AirQualityMonitoringScreen extends StatefulWidget {
const AirQualityMonitoringScreen({super.key});
@override
State<AirQualityMonitoringScreen> createState() => _AirQualityMonitoringScreenState();
State<AirQualityMonitoringScreen> createState() =>
_AirQualityMonitoringScreenState();
}
class _AirQualityMonitoringScreenState extends State<AirQualityMonitoringScreen> {
class _AirQualityMonitoringScreenState
extends State<AirQualityMonitoringScreen> {
String _selectedPeriod = "Hari Ini";
@override
@ -61,8 +63,30 @@ class _AirQualityMonitoringScreenState extends State<AirQualityMonitoringScreen>
csvContent.writeln("Jam,AQI (Air Quality Index)");
final dailyData = [
68.0, 70.0, 72.0, 75.0, 78.0, 80.0, 78.0, 75.0, 72.0, 70.0,
68.0, 65.0, 60.0, 58.0, 60.0, 62.0, 65.0, 68.0, 70.0, 72.0, 75.0, 78.0, 76.0, 74.0
68.0,
70.0,
72.0,
75.0,
78.0,
80.0,
78.0,
75.0,
72.0,
70.0,
68.0,
65.0,
60.0,
58.0,
60.0,
62.0,
65.0,
68.0,
70.0,
72.0,
75.0,
78.0,
76.0,
74.0
];
for (int i = 0; i < dailyData.length; i++) {
csvContent.writeln("$i:00,${dailyData[i]}");
@ -75,7 +99,8 @@ class _AirQualityMonitoringScreenState extends State<AirQualityMonitoringScreen>
csvContent.writeln("Maksimal: 80 AQI");
csvContent.writeln("Minimal: 58 AQI");
showExportPreview(context, csvContent.toString(), "Data Harian Kualitas Udara");
showExportPreview(
context, csvContent.toString(), "Data Harian Kualitas Udara");
}
void _exportWeeklyAirQualityData() {
@ -85,7 +110,15 @@ class _AirQualityMonitoringScreenState extends State<AirQualityMonitoringScreen>
csvContent.writeln("");
csvContent.writeln("Hari,Rata-rata AQI");
final days = ['Senin', 'Selasa', 'Rabu', 'Kamis', 'Jumat', 'Sabtu', 'Minggu'];
final days = [
'Senin',
'Selasa',
'Rabu',
'Kamis',
'Jumat',
'Sabtu',
'Minggu'
];
final weeklyData = [68.0, 72.0, 75.0, 78.0, 72.0, 68.0, 62.0];
for (int i = 0; i < days.length; i++) {
@ -99,13 +132,15 @@ class _AirQualityMonitoringScreenState extends State<AirQualityMonitoringScreen>
csvContent.writeln("Maksimal: 78 AQI");
csvContent.writeln("Minimal: 62 AQI");
showExportPreview(context, csvContent.toString(), "Data Mingguan Kualitas Udara");
showExportPreview(
context, csvContent.toString(), "Data Mingguan Kualitas Udara");
}
void _exportMonthlyAirQualityData() {
final StringBuffer csvContent = StringBuffer();
csvContent.writeln("DATA MONITORING KUALITAS UDARA - BULANAN");
csvContent.writeln("Bulan: ${DateTime.now().toString().split(' ')[0].substring(0, 7)}");
csvContent.writeln(
"Bulan: ${DateTime.now().toString().split(' ')[0].substring(0, 7)}");
csvContent.writeln("");
csvContent.writeln("Tanggal,Rata-rata AQI");
@ -121,7 +156,8 @@ class _AirQualityMonitoringScreenState extends State<AirQualityMonitoringScreen>
csvContent.writeln("Maksimal: 82 AQI");
csvContent.writeln("Minimal: 55 AQI");
showExportPreview(context, csvContent.toString(), "Data Bulanan Kualitas Udara");
showExportPreview(
context, csvContent.toString(), "Data Bulanan Kualitas Udara");
}
void _exportAnnualAirQualityData() {
@ -132,11 +168,47 @@ class _AirQualityMonitoringScreenState extends State<AirQualityMonitoringScreen>
csvContent.writeln("Bulan,Rata-rata AQI,AQI Max");
final months = [
'Januari', 'Februari', 'Maret', 'April', 'Mei', 'Juni',
'Juli', 'Agustus', 'September', 'Oktober', 'November', 'Desember'
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember'
];
final averages = [
68.0,
70.0,
72.5,
75.0,
78.0,
80.0,
82.0,
80.0,
75.0,
70.0,
68.0,
65.0
];
final maxAqi = [
85.0,
88.0,
90.0,
95.0,
98.0,
100.0,
102.0,
100.0,
95.0,
90.0,
85.0,
80.0
];
final averages = [68.0, 70.0, 72.5, 75.0, 78.0, 80.0, 82.0, 80.0, 75.0, 70.0, 68.0, 65.0];
final maxAqi = [85.0, 88.0, 90.0, 95.0, 98.0, 100.0, 102.0, 100.0, 95.0, 90.0, 85.0, 80.0];
for (int i = 0; i < months.length; i++) {
csvContent.writeln("${months[i]},${averages[i]},${maxAqi[i]}");
@ -148,7 +220,8 @@ class _AirQualityMonitoringScreenState extends State<AirQualityMonitoringScreen>
csvContent.writeln("AQI Max Teringgi: 102.0");
csvContent.writeln("AQI Min Terendah: 65.0");
showExportPreview(context, csvContent.toString(), "Data Tahunan Kualitas Udara");
showExportPreview(
context, csvContent.toString(), "Data Tahunan Kualitas Udara");
}
/// DATA GENERATORS
@ -157,8 +230,30 @@ class _AirQualityMonitoringScreenState extends State<AirQualityMonitoringScreen>
case "Hari Ini":
return List.generate(24, (i) {
final values = [
68.0, 70.0, 72.0, 75.0, 78.0, 80.0, 78.0, 75.0, 72.0, 70.0,
68.0, 65.0, 60.0, 58.0, 60.0, 62.0, 65.0, 68.0, 70.0, 72.0, 75.0, 78.0, 76.0, 74.0
68.0,
70.0,
72.0,
75.0,
78.0,
80.0,
78.0,
75.0,
72.0,
70.0,
68.0,
65.0,
60.0,
58.0,
60.0,
62.0,
65.0,
68.0,
70.0,
72.0,
75.0,
78.0,
76.0,
74.0
];
return FlSpot(i.toDouble(), values[i]);
});
@ -244,7 +339,8 @@ class _AirQualityMonitoringScreenState extends State<AirQualityMonitoringScreen>
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: ["Hari Ini", "Minggu Ini", "Bulan Ini"].map((period) {
children:
["Hari Ini", "Minggu Ini", "Bulan Ini"].map((period) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
@ -504,8 +600,18 @@ class _AirQualityMonitoringScreenState extends State<AirQualityMonitoringScreen>
showTitles: true,
getTitlesWidget: (value, meta) {
final months = [
'Jan', 'Feb', 'Mar', 'Apr', 'Mei', 'Jun',
'Jul', 'Agu', 'Sep', 'Okt', 'Nov', 'Des'
'Jan',
'Feb',
'Mar',
'Apr',
'Mei',
'Jun',
'Jul',
'Agu',
'Sep',
'Okt',
'Nov',
'Des'
];
return Text(
months[value.toInt()],
@ -588,6 +694,7 @@ class _AirQualityMonitoringScreenState extends State<AirQualityMonitoringScreen>
color: Colors.orange.withOpacity(0.1),
),
),
/// AQI Max
LineChartBarData(
spots: const [

View File

@ -3,7 +3,7 @@ import 'package:fl_chart/fl_chart.dart';
import 'package:firebase_database/firebase_database.dart';
import 'dart:async';
import 'package:intl/intl.dart';
import 'widgets/monitoring_shared.dart';
import 'shared/widgets/monitoring_shared.dart';
/// EVAPORASI MONITORING SCREEN
class EvaporasiMonitoringScreen extends StatefulWidget {

View File

@ -0,0 +1,15 @@
import 'package:monitoring_repository/monitoring_repository.dart';
class WindSpeedBloc {
final MonitoringRepository _repo;
WindSpeedBloc({required MonitoringRepository repo}) : _repo = repo;
void startListening() {
// 3. Sekarang kamu bisa akses _repo di dalam lingkup class ini
_repo.getSensorStream('anemometer/realtime').listen((event) {
// Olah datanya di sini
print(event.snapshot.value);
});
}
}

View File

@ -0,0 +1,226 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../shared/widgets/monitoring_shared.dart';
class WindSpeedAnnualSection extends StatelessWidget {
final VoidCallback onExportAnnual;
const WindSpeedAnnualSection({
super.key,
required this.onExportAnnual,
});
final double _rataRata = 0.0;
final double _kecepatanMax = 0.0;
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Statistik Tahunan Kecepatan Angin",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 15),
Row(
children: [
Expanded(
child: StatCard(
title: "Rata-rata Tahunan",
value: this._rataRata.toStringAsFixed(2),
color: const Color.fromARGB(255, 76, 120, 175),
icon: Icons.air,
),
),
SizedBox(width: 10),
Expanded(
child: StatCard(
title: "Kecepatan Max",
value: this._kecepatanMax.toStringAsFixed(2),
color: const Color.fromARGB(255, 109, 109, 109),
icon: Icons.trending_up,
),
),
],
),
const SizedBox(height: 20),
const Text(
"Grafik Tahunan - Rata-rata & Kecepatan Maksimal",
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 12),
Card(
elevation: 3,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: SizedBox(
height: 320,
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: false,
horizontalInterval: 2,
getDrawingHorizontalLine: (value) {
return FlLine(
color: const Color.fromARGB(255, 224, 224, 224),
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
topTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
rightTitles: AxisTitles(
sideTitles: SideTitles(showTitles: false),
),
),
borderData: FlBorderData(
show: true,
border: Border(
left: BorderSide(
color: Colors.grey.shade300,
width: 1,
),
bottom: BorderSide(
color: Colors.grey.shade300,
width: 1,
),
),
),
lineBarsData: [
LineChartBarData(
spots: const [
FlSpot(0, 5.5),
FlSpot(1, 6.2),
FlSpot(2, 6.8),
FlSpot(3, 7.5),
FlSpot(4, 7.8),
FlSpot(5, 8.0),
FlSpot(6, 8.2),
FlSpot(7, 8.0),
FlSpot(8, 7.2),
FlSpot(9, 6.5),
FlSpot(10, 5.8),
FlSpot(11, 5.2),
],
isCurved: true,
barWidth: 2.5,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: Colors.green,
strokeWidth: 0,
);
},
),
color: Colors.green,
belowBarData: BarAreaData(
show: true,
color: Colors.green.withOpacity(0.1),
),
),
LineChartBarData(
spots: const [
FlSpot(0, 12.0),
FlSpot(1, 13.5),
FlSpot(2, 14.0),
FlSpot(3, 15.5),
FlSpot(4, 16.0),
FlSpot(5, 16.2),
FlSpot(6, 16.5),
FlSpot(7, 16.0),
FlSpot(8, 15.0),
FlSpot(9, 14.0),
FlSpot(10, 12.5),
FlSpot(11, 11.0),
],
isCurved: true,
barWidth: 2.5,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: Colors.red,
strokeWidth: 0,
);
},
),
color: Colors.red,
),
],
),
),
),
),
),
const SizedBox(height: 15),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.green,
borderRadius: BorderRadius.circular(3),
),
),
const SizedBox(width: 8),
const Text(
"Rata-rata (km/h)",
style: TextStyle(fontSize: 12),
),
const SizedBox(width: 20),
Container(
width: 16,
height: 16,
decoration: BoxDecoration(
color: Colors.red,
borderRadius: BorderRadius.circular(3),
),
),
const SizedBox(width: 8),
const Text(
"Kecepatan Max (km/h)",
style: TextStyle(fontSize: 12),
),
],
),
const SizedBox(height: 15),
Center(
child: ElevatedButton.icon(
onPressed: onExportAnnual,
icon: const Icon(Icons.file_download),
label: const Text("Download Excel Tahunan"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.green,
foregroundColor: Colors.white,
),
),
),
],
);
}
}

View File

@ -8,10 +8,16 @@ class WindSpeedExplanation extends StatelessWidget {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.green.shade50,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.green.shade200),
),
color: const Color.fromARGB(255, 232, 240, 245),
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.2),
blurRadius: 3,
spreadRadius: 0,
offset: const Offset(0, 5),
)
]),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
@ -20,7 +26,7 @@ class WindSpeedExplanation extends StatelessWidget {
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.bold,
color: Colors.green,
color: Color.fromARGB(255, 0, 0, 0),
),
),
SizedBox(height: 10),
@ -29,7 +35,7 @@ class WindSpeedExplanation extends StatelessWidget {
style: TextStyle(
fontSize: 12,
height: 1.6,
color: Colors.green,
color: Color.fromARGB(255, 0, 0, 0),
),
),
],

View File

@ -0,0 +1,253 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../shared/widgets/monitoring_shared.dart';
class WindSpeedGraphBlock extends StatelessWidget {
final String selectedPeriod;
final List<double> dailySpeeds;
final bool isOnline;
final ValueChanged<String> onPeriodChanged;
final ValueChanged<String> onExportPressed;
const WindSpeedGraphBlock({
super.key,
required this.selectedPeriod,
required this.dailySpeeds,
required this.isOnline,
required this.onPeriodChanged,
required this.onExportPressed,
});
List<FlSpot> _getPeriodSpots() {
switch (selectedPeriod) {
case "Hari Ini":
return List.generate(24, (i) {
final value = dailySpeeds[i];
return FlSpot(i.toDouble(), value);
});
case "Minggu Ini":
return const [
FlSpot(0, 6.5),
FlSpot(1, 7.2),
FlSpot(2, 7.8),
FlSpot(3, 8.0),
FlSpot(4, 7.5),
FlSpot(5, 6.8),
FlSpot(6, 5.5),
];
case "Bulan Ini":
return const [
FlSpot(0, 6.0),
FlSpot(1, 6.5),
FlSpot(2, 7.0),
FlSpot(3, 7.5),
FlSpot(4, 7.8),
FlSpot(5, 8.0),
FlSpot(6, 8.2),
FlSpot(7, 8.0),
FlSpot(8, 7.5),
FlSpot(9, 7.0),
FlSpot(10, 6.5),
FlSpot(11, 6.0),
];
default:
return [];
}
}
@override
Widget build(BuildContext context) {
return Card(
elevation: 4,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
"Status Kecepatan Angin",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color:
isOnline ? Colors.green.shade100 : Colors.red.shade100,
borderRadius: BorderRadius.circular(20),
),
child: Text(
isOnline ? "ONLINE" : "OFFLINE",
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: isOnline ? Colors.green : Colors.red,
),
),
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children:
["Hari Ini", "Minggu Ini", "Bulan Ini"].map((period) {
return Padding(
padding: const EdgeInsets.only(right: 8),
child: ChoiceChip(
label: Text(period),
selected: selectedPeriod == period,
onSelected: (selected) {
if (selected) {
onPeriodChanged(period);
}
},
side: BorderSide.none,
backgroundColor: Colors.grey.withOpacity(0.1),
selectedColor:
const Color.fromARGB(255, 0, 136, 255),
labelStyle: TextStyle(
color: selectedPeriod == period
? Colors.white
: Colors.black,
),
),
);
}).toList(),
),
),
),
const SizedBox(width: 8),
ElevatedButton.icon(
onPressed: () => onExportPressed(selectedPeriod),
icon: const Icon(Icons.download, size: 16),
label: const Text("Export"),
style: ElevatedButton.styleFrom(
backgroundColor: const Color.fromARGB(255, 0, 145, 255),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 6,
),
textStyle: const TextStyle(fontSize: 11),
),
),
],
),
const SizedBox(height: 16),
SizedBox(
height: 300,
child: LineChart(
LineChartData(
gridData: FlGridData(
show: true,
drawHorizontalLine: true,
drawVerticalLine: false,
horizontalInterval: 1,
getDrawingHorizontalLine: (value) {
return FlLine(
color: Colors.grey.shade300,
strokeWidth: 1,
);
},
),
titlesData: FlTitlesData(
show: true,
bottomTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: getPeriodInterval(selectedPeriod),
getTitlesWidget: (value, meta) {
return Text(
getPeriodLabel(selectedPeriod, value),
style: const TextStyle(
fontSize: 10,
color: Colors.grey,
),
);
},
),
),
leftTitles: AxisTitles(
sideTitles: SideTitles(
showTitles: true,
interval: 1,
getTitlesWidget: (value, meta) {
return Text(
value.toInt().toString(),
style: const TextStyle(
fontSize: 9,
color: Colors.grey,
),
);
},
reservedSize: 28,
),
),
topTitles:
AxisTitles(sideTitles: SideTitles(showTitles: false)),
rightTitles:
AxisTitles(sideTitles: SideTitles(showTitles: false)),
),
borderData: FlBorderData(
show: true,
border: Border(
left: BorderSide(
color: Colors.grey.shade300,
width: 1,
),
bottom: BorderSide(
color: Colors.grey.shade300,
width: 1,
),
),
),
lineBarsData: [
LineChartBarData(
spots: _getPeriodSpots(),
isCurved: true,
barWidth: 2.5,
dotData: FlDotData(
show: true,
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: const Color.fromARGB(255, 0, 191, 255),
strokeWidth: 0,
);
},
),
color: const Color.fromARGB(255, 0, 191, 255),
belowBarData: BarAreaData(
show: true,
color: const Color.fromARGB(255, 0, 115, 255)
.withOpacity(0.1),
),
),
],
),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,95 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class WindSpeedStatusBlock extends StatelessWidget {
final double currentSpeed;
final bool isOnline;
final DateTime lastUpdateTime;
const WindSpeedStatusBlock({
super.key,
required this.currentSpeed,
required this.isOnline,
required this.lastUpdateTime,
});
@override
Widget build(BuildContext context) {
final formattedTime =
DateFormat("dd MMM yyyy • HH:mm:ss").format(lastUpdateTime);
/// ====== Current Speed bagian kode kecepatan angin ===== ///
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.grey.withOpacity(0.12),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Kecepatan Angin",
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${currentSpeed.toStringAsFixed(2)} km/h",
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Color.fromARGB(255, 0, 0, 0),
),
),
/// ==== bagian online offline status tampilan ==== ///
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color:
isOnline ? Colors.green.shade100 : Colors.red.shade100,
borderRadius: BorderRadius.circular(20),
),
child: Text(
isOnline ? "ONLINE" : "OFFLINE",
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: isOnline ? Colors.green : Colors.red,
),
),
),
],
),
const SizedBox(height: 10),
Text(
"Update: $formattedTime",
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
),
);
}
}

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../widgets/monitoring_shared.dart';
import '../../shared/widgets/monitoring_shared.dart';
class WindSpeedAnnualSection extends StatelessWidget {
final VoidCallback onExportAnnual;
@ -31,7 +31,7 @@ class WindSpeedAnnualSection extends StatelessWidget {
child: StatCard(
title: "Rata-rata Tahunan",
value: this._rataRata.toStringAsFixed(2),
color: Colors.green,
color: const Color.fromARGB(255, 76, 120, 175),
icon: Icons.air,
),
),
@ -40,7 +40,7 @@ class WindSpeedAnnualSection extends StatelessWidget {
child: StatCard(
title: "Kecepatan Max",
value: this._kecepatanMax.toStringAsFixed(2),
color: Colors.red,
color: const Color.fromARGB(255, 109, 109, 109),
icon: Icons.trending_up,
),
),
@ -73,7 +73,7 @@ class WindSpeedAnnualSection extends StatelessWidget {
horizontalInterval: 2,
getDrawingHorizontalLine: (value) {
return FlLine(
color: Colors.grey.shade300,
color: const Color.fromARGB(255, 224, 224, 224),
strokeWidth: 1,
);
},
@ -142,7 +142,7 @@ class WindSpeedAnnualSection extends StatelessWidget {
),
LineChartBarData(
spots: const [
FlSpot(0, 12.0), //sadds
FlSpot(0, 12.0),
FlSpot(1, 13.5),
FlSpot(2, 14.0),
FlSpot(3, 15.5),

View File

@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:fl_chart/fl_chart.dart';
import '../../widgets/monitoring_shared.dart';
import '../../shared/widgets/monitoring_shared.dart';
class WindSpeedGraphBlock extends StatelessWidget {
final String selectedPeriod;
@ -119,7 +119,10 @@ class WindSpeedGraphBlock extends StatelessWidget {
onPeriodChanged(period);
}
},
selectedColor: Colors.green,
side: BorderSide.none,
backgroundColor: Colors.grey.withOpacity(0.1),
selectedColor:
const Color.fromARGB(255, 0, 136, 255),
labelStyle: TextStyle(
color: selectedPeriod == period
? Colors.white
@ -137,7 +140,7 @@ class WindSpeedGraphBlock extends StatelessWidget {
icon: const Icon(Icons.download, size: 16),
label: const Text("Export"),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.orange,
backgroundColor: const Color.fromARGB(255, 0, 145, 255),
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(
horizontal: 10,
@ -226,15 +229,16 @@ class WindSpeedGraphBlock extends StatelessWidget {
getDotPainter: (spot, percent, barData, index) {
return FlDotCirclePainter(
radius: 4,
color: Colors.green,
color: const Color.fromARGB(255, 0, 191, 255),
strokeWidth: 0,
);
},
),
color: Colors.green,
color: const Color.fromARGB(255, 0, 191, 255),
belowBarData: BarAreaData(
show: true,
color: Colors.green.withOpacity(0.1),
color: const Color.fromARGB(255, 0, 115, 255)
.withOpacity(0.1),
),
),
],

View File

@ -1,68 +0,0 @@
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
class WindSpeedStatusBlock extends StatelessWidget {
final double currentSpeed;
final bool isOnline;
final DateTime lastUpdateTime;
const WindSpeedStatusBlock({
super.key,
required this.currentSpeed,
required this.isOnline,
required this.lastUpdateTime,
});
@override
Widget build(BuildContext context) {
final formattedTime =
DateFormat("dd MMM yyyy • HH:mm:ss").format(lastUpdateTime);
return Padding(
padding: const EdgeInsets.only(bottom: 12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
"${currentSpeed.toStringAsFixed(2)} km/h",
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.green,
),
),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 5,
),
decoration: BoxDecoration(
color: isOnline ? Colors.green.shade100 : Colors.red.shade100,
borderRadius: BorderRadius.circular(20),
),
child: Text(
isOnline ? "ONLINE" : "OFFLINE",
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: isOnline ? Colors.green : Colors.red,
),
),
),
],
),
const SizedBox(height: 4),
Text(
"Update: $formattedTime",
style: const TextStyle(
fontSize: 12,
color: Colors.grey,
),
),
],
),
);
}
}

View File

@ -0,0 +1,272 @@
import 'dart:async';
import '../';
import 'package:flutter/material.dart';
import '../shared/widgets/monitoring_shared.dart';
import 'views/annual_section.dart';
import 'views/explanation.dart';
import 'views/graph_block.dart';
import 'views/status_block.dart';
import 'package:google_fonts/google_fonts.dart';
class WindSpeedMonitoringScreen extends StatefulWidget {
const WindSpeedMonitoringScreen({super.key});
@override
State<WindSpeedMonitoringScreen> createState() =>
_WindSpeedMonitoringScreenState();
}
class _WindSpeedMonitoringScreenState extends State<WindSpeedMonitoringScreen> {
String _selectedPeriod = "Hari Ini";
// realtime values
DateTime? _lastUpdateTime;
double _currentSpeed = 0.0;
final List<double> _dailySpeeds = List<double>.filled(24, 0.0);
// hourly aggregation helpers (PRO version)
double _hourlyTotalSpeed = 0.0;
int _hourlyCount = 0;
int _currentHour = DateTime.now().hour;
bool get _isOnline {
if (_lastUpdateTime == null) return false;
final diff = DateTime.now().difference(_lastUpdateTime!);
return diff.inSeconds <= 5;
}
@override
void initState() {
super.initState();
_realtimeSub = _realtimeRef.onValue.listen(_onRealtimeData);
}
void _onRealtimeData(DatabaseEvent event) {
final data = event.snapshot.value;
if (data is Map) {
final speed = (data['kecepatan'] ?? 0).toDouble();
final timestamp = data['timestamp'] ?? 0;
final waktu =
DateTime.fromMillisecondsSinceEpoch(timestamp * 1000).toLocal();
setState(() {
_currentSpeed = speed;
_lastUpdateTime = waktu;
final incomingHour = waktu.hour;
if (incomingHour != _currentHour) {
if (_hourlyCount > 0) {
final avg = _hourlyTotalSpeed / _hourlyCount;
_dailySpeeds[_currentHour] = avg;
}
_hourlyTotalSpeed = 0.0;
_hourlyCount = 0;
_currentHour = incomingHour;
if (incomingHour == 0) {
for (int i = 0; i < 24; i++) {
_dailySpeeds[i] = 0.0;
}
}
}
_hourlyTotalSpeed += speed;
_hourlyCount++;
});
}
}
@override
void dispose() {
_realtimeSub?.cancel();
super.dispose();
}
void _handleExportSelectedPeriod(String period) {
switch (period) {
case "Hari Ini":
_exportDailyWindData();
break;
case "Minggu Ini":
_exportWeeklyWindData();
break;
case "Bulan Ini":
_exportMonthlyWindData();
break;
}
}
void _exportDailyWindData() {
final StringBuffer csvContent = StringBuffer();
csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - HARIAN (24 JAM)");
csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}");
csvContent.writeln("");
csvContent.writeln("Jam,Kecepatan (km/h)");
final dailyData = [];
for (int i = 0; i < dailyData.length; i++) {
csvContent.writeln("$i:00,${dailyData[i]}");
}
csvContent.writeln("");
csvContent.writeln("Statistik Harian");
csvContent.writeln("Total: 165.8 km/h");
csvContent.writeln("Rata-rata: 6.9 km/h");
csvContent.writeln("Maksimal: 11.0 km/h");
csvContent.writeln("Minimal: 4.3 km/h");
showExportPreview(
context, csvContent.toString(), "Data Harian Kecepatan Angin");
}
void _exportWeeklyWindData() {
final StringBuffer csvContent = StringBuffer();
csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - MINGGUAN");
csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}");
csvContent.writeln("");
csvContent.writeln("Hari,Rata-rata (km/h)");
final days = [
'Senin',
'Selasa',
'Rabu',
'Kamis',
'Jumat',
'Sabtu',
'Minggu'
];
final weeklyData = [6.5, 7.2, 7.8, 8.0, 7.5, 6.8, 5.5];
for (int i = 0; i < days.length; i++) {
csvContent.writeln("${days[i]},${weeklyData[i]}");
}
csvContent.writeln("");
csvContent.writeln("Statistik Mingguan");
csvContent.writeln("Total: 49.3 km/h");
csvContent.writeln("Rata-rata: 7.0 km/h");
csvContent.writeln("Maksimal: 8.0 km/h");
csvContent.writeln("Minimal: 5.5 km/h");
showExportPreview(
context, csvContent.toString(), "Data Mingguan Kecepatan Angin");
}
void _exportMonthlyWindData() {
final StringBuffer csvContent = StringBuffer();
csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - BULANAN");
csvContent.writeln(
"Bulan: ${DateTime.now().toString().split(' ')[0].substring(0, 7)}");
csvContent.writeln("");
csvContent.writeln("Tanggal,Rata-rata (km/h)");
for (int i = 1; i <= 28; i++) {
final value = (6.0 + (i % 6) * 0.8).toStringAsFixed(1);
csvContent.writeln("$i,$value");
}
csvContent.writeln("");
csvContent.writeln("Statistik Bulanan");
csvContent.writeln("Total: 180.5 km/h");
csvContent.writeln("Rata-rata: 6.4 km/h");
csvContent.writeln("Maksimal: 9.0 km/h");
csvContent.writeln("Minimal: 4.0 km/h");
showExportPreview(
context, csvContent.toString(), "Data Bulanan Kecepatan Angin");
}
void _exportAnnualWindData() {
final StringBuffer csvContent = StringBuffer();
csvContent.writeln("DATA MONITORING KECEPATAN ANGIN - TAHUNAN");
csvContent.writeln("Tahun: 2025");
csvContent.writeln("");
csvContent.writeln("Bulan,Rata-rata (km/h),Kecepatan Max (km/h)");
final months = [
'Januari',
'Februari',
'Maret',
'April',
'Mei',
'Juni',
'Juli',
'Agustus',
'September',
'Oktober',
'November',
'Desember'
];
final averages = [];
final maxSpeed = [];
for (int i = 0; i < months.length; i++) {
csvContent.writeln("${months[i]},${averages[i]},${maxSpeed[i]}");
}
csvContent.writeln("");
csvContent.writeln("STATISTIK TAHUNAN");
csvContent.writeln("Rata-rata Tahunan: 7.1 km/h");
csvContent.writeln("Kecepatan Max Tertinggi: 16.5 km/h");
csvContent.writeln("Kecepatan Min Terendah: 5.2 km/h");
showExportPreview(
context, csvContent.toString(), "Data Tahunan Kecepatan Angin");
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(
"Wind Speed",
style: GoogleFonts.rubik(
color: Colors.black, fontWeight: FontWeight.bold),
),
centerTitle: true,
backgroundColor: const Color.fromARGB(37, 158, 158, 158),
),
body: Padding(
padding: const EdgeInsets.all(16),
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (_lastUpdateTime != null)
WindSpeedStatusBlock(
currentSpeed: _currentSpeed,
isOnline: _isOnline,
lastUpdateTime: _lastUpdateTime!,
),
const SizedBox(height: 20),
WindSpeedGraphBlock(
selectedPeriod: _selectedPeriod,
dailySpeeds: _dailySpeeds,
isOnline: _isOnline,
onPeriodChanged: (period) {
setState(() {
_selectedPeriod = period;
});
},
onExportPressed: _handleExportSelectedPeriod,
),
const SizedBox(height: 25),
const WindSpeedExplanation(),
const SizedBox(height: 25),
WindSpeedAnnualSection(
onExportAnnual: _exportAnnualWindData,
),
],
),
),
),
);
}
}

View File

@ -3,11 +3,11 @@ import 'dart:async';
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import '../widgets/monitoring_shared.dart';
import 'widgets/wind_speed_annual_section.dart';
import 'widgets/wind_speed_explanation.dart';
import 'widgets/wind_speed_graph_block.dart';
import 'widgets/wind_speed_status_block.dart';
import '../shared/widgets/monitoring_shared.dart';
import 'views/annual_section.dart';
import 'views/explanation.dart';
import 'views/graph_block.dart';
import 'views/status_block.dart';
import 'package:google_fonts/google_fonts.dart';
class WindSpeedMonitoringScreen extends StatefulWidget {
@ -21,21 +21,6 @@ class WindSpeedMonitoringScreen extends StatefulWidget {
class _WindSpeedMonitoringScreenState extends State<WindSpeedMonitoringScreen> {
String _selectedPeriod = "Hari Ini";
// realtime database state
final DatabaseReference _realtimeRef =
FirebaseDatabase.instance.ref('anemometer/realtime');
StreamSubscription<DatabaseEvent>? _realtimeSub;
// realtime values
DateTime? _lastUpdateTime;
double _currentSpeed = 0.0;
final List<double> _dailySpeeds = List<double>.filled(24, 0.0);
// hourly aggregation helpers (PRO version)
double _hourlyTotalSpeed = 0.0;
int _hourlyCount = 0;
int _currentHour = DateTime.now().hour;
bool get _isOnline {
if (_lastUpdateTime == null) return false;
final diff = DateTime.now().difference(_lastUpdateTime!);
@ -55,7 +40,8 @@ class _WindSpeedMonitoringScreenState extends State<WindSpeedMonitoringScreen> {
final speed = (data['kecepatan'] ?? 0).toDouble();
final timestamp = data['timestamp'] ?? 0;
final waktu = DateTime.fromMillisecondsSinceEpoch(timestamp * 1000);
final waktu =
DateTime.fromMillisecondsSinceEpoch(timestamp * 1000).toLocal();
setState(() {
_currentSpeed = speed;
@ -113,32 +99,7 @@ class _WindSpeedMonitoringScreenState extends State<WindSpeedMonitoringScreen> {
csvContent.writeln("");
csvContent.writeln("Jam,Kecepatan (km/h)");
final dailyData = [
5.0,
5.2,
4.8,
4.5,
4.3,
4.6,
5.1,
6.0,
7.2,
8.5,
9.0,
10.2,
11.0,
10.5,
10.0,
9.2,
8.0,
6.5,
5.5,
5.0,
4.8,
4.5,
5.0,
5.2
];
final dailyData = [];
for (int i = 0; i < dailyData.length; i++) {
csvContent.writeln("$i:00,${dailyData[i]}");
@ -233,34 +194,8 @@ class _WindSpeedMonitoringScreenState extends State<WindSpeedMonitoringScreen> {
'November',
'Desember'
];
final averages = [
5.5,
6.2,
6.8,
7.5,
7.8,
8.0,
8.2,
8.0,
7.2,
6.5,
5.8,
5.2
];
final maxSpeed = [
12.0,
13.5,
14.0,
15.5,
16.0,
16.2,
16.5,
16.0,
15.0,
14.0,
12.5,
11.0
];
final averages = [];
final maxSpeed = [];
for (int i = 0; i < months.length; i++) {
csvContent.writeln("${months[i]},${averages[i]},${maxSpeed[i]}");
@ -294,20 +229,13 @@ class _WindSpeedMonitoringScreenState extends State<WindSpeedMonitoringScreen> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"Kecepatan Angin",
style: TextStyle(
fontSize: 22,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
if (_lastUpdateTime != null)
WindSpeedStatusBlock(
currentSpeed: _currentSpeed,
isOnline: _isOnline,
lastUpdateTime: _lastUpdateTime!,
),
const SizedBox(height: 20),
WindSpeedGraphBlock(
selectedPeriod: _selectedPeriod,
dailySpeeds: _dailySpeeds,

View File

@ -0,0 +1,4 @@
library monitoring_repository;
export 'src/monitoring_repo.dart';
export 'src/firebase_monitoring_repo.dart';

View File

@ -0,0 +1,23 @@
import 'dart:developer';
import 'dart:async';
import 'package:rxdart/rxdart.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
/// === ambil data dari realtime database firebase === ///
class FirebaseMonitoringRepo implements MonitoringRepository {
final FirebaseDatabase _db = FirebaseDatabase.instance;
// Satu fungsi untuk semua jenis sensor
// Kamu cukup masukkan "path" database-nya saja
@override
Stream<DatabaseEvent> getSensorStream(String path) {
return _db.ref(path).onValue;
}
@override
// Jika ingin mengambil data sekali saja (bukan stream)
Future<DataSnapshot> getSensorSnapshot(String path) async {
return await _db.ref(path).get();
}
}

View File

@ -0,0 +1 @@
export 'wind_speed.dart';

View File

@ -0,0 +1,21 @@
class MyWindSpeed {
double speed;
int pulse;
DateTime timestamp;
MyWindSpeed({
required this.speed,
required this.pulse,
required this.timestamp,
});
factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) {
return MyWindSpeed(
speed: (json['speed'] ?? 0).toDouble(),
pulse: (json['pulse'] ?? 0) as int,
timestamp: DateTime.fromMillisecondsSinceEpoch(
(json['timestamp'] ?? 0) * 1000, // kalau dari Unix detik
),
);
}
}

View File

@ -0,0 +1,7 @@
import 'package:firebase_database/firebase_database.dart';
abstract class MonitoringRepository {
Stream<DatabaseEvent> getSensorStream(String path);
Future<DataSnapshot> getSensorSnapshot(String path);
}

View File

@ -0,0 +1,290 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: f698de6eb8a0dd7a9a931bbfe13568e8b77e702eb2deb13dd83480c5373e7746
url: "https://pub.dev"
source: hosted
version: "1.3.68"
async:
dependency: transitive
description:
name: async
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
url: "https://pub.dev"
source: hosted
version: "2.13.1"
boolean_selector:
dependency: transitive
description:
name: boolean_selector
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
url: "https://pub.dev"
source: hosted
version: "2.1.2"
characters:
dependency: transitive
description:
name: characters
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
url: "https://pub.dev"
source: hosted
version: "1.4.0"
clock:
dependency: transitive
description:
name: clock
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
url: "https://pub.dev"
source: hosted
version: "1.1.2"
collection:
dependency: transitive
description:
name: collection
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
url: "https://pub.dev"
source: hosted
version: "1.19.1"
fake_async:
dependency: transitive
description:
name: fake_async
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
url: "https://pub.dev"
source: hosted
version: "1.3.3"
firebase_core:
dependency: transitive
description:
name: firebase_core
sha256: "2f988dab915efde3b3105268dbd69efce0e8570d767a218ccd914afd0c10c8cc"
url: "https://pub.dev"
source: hosted
version: "4.6.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: "0ecda14c1bfc9ed8cac303dd0f8d04a320811b479362a9a4efb14fd331a473ce"
url: "https://pub.dev"
source: hosted
version: "6.0.3"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: "1399ab1f0ac3b503d8a9be64a4c997fc066bbf33f701f42866e5569f26205ebe"
url: "https://pub.dev"
source: hosted
version: "3.5.1"
firebase_database:
dependency: "direct main"
description:
name: firebase_database
sha256: "05358855166c943a2fe9321bce69ab6b583c9a49e70e01b5297ef03f8bea50b9"
url: "https://pub.dev"
source: hosted
version: "12.2.0"
firebase_database_platform_interface:
dependency: transitive
description:
name: firebase_database_platform_interface
sha256: "83278cc315ad0f36a5e30fc7f3d1c6edc722340347d41ea17f57e52176bb98f8"
url: "https://pub.dev"
source: hosted
version: "0.3.1"
firebase_database_web:
dependency: transitive
description:
name: firebase_database_web
sha256: "77df7f7c59f4bba33f06644f76e28a8acc594487f1aaa4c79947abf491a7ae70"
url: "https://pub.dev"
source: hosted
version: "0.2.7+5"
flutter:
dependency: "direct main"
description: flutter
source: sdk
version: "0.0.0"
flutter_lints:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
url: "https://pub.dev"
source: hosted
version: "6.0.0"
flutter_test:
dependency: "direct dev"
description: flutter
source: sdk
version: "0.0.0"
flutter_web_plugins:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
leak_tracker:
dependency: transitive
description:
name: leak_tracker
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
url: "https://pub.dev"
source: hosted
version: "11.0.2"
leak_tracker_flutter_testing:
dependency: transitive
description:
name: leak_tracker_flutter_testing
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
url: "https://pub.dev"
source: hosted
version: "3.0.10"
leak_tracker_testing:
dependency: transitive
description:
name: leak_tracker_testing
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
url: "https://pub.dev"
source: hosted
version: "3.0.2"
lints:
dependency: transitive
description:
name: lints
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
url: "https://pub.dev"
source: hosted
version: "6.1.0"
matcher:
dependency: transitive
description:
name: matcher
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
url: "https://pub.dev"
source: hosted
version: "0.12.17"
material_color_utilities:
dependency: transitive
description:
name: material_color_utilities
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
url: "https://pub.dev"
source: hosted
version: "0.11.1"
meta:
dependency: transitive
description:
name: meta
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
url: "https://pub.dev"
source: hosted
version: "1.17.0"
path:
dependency: transitive
description:
name: path
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
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"
rxdart:
dependency: "direct main"
description:
name: rxdart
sha256: "0c7c0cedd93788d996e33041ffecda924cc54389199cde4e6a34b440f50044cb"
url: "https://pub.dev"
source: hosted
version: "0.27.7"
sky_engine:
dependency: transitive
description: flutter
source: sdk
version: "0.0.0"
source_span:
dependency: transitive
description:
name: source_span
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
url: "https://pub.dev"
source: hosted
version: "1.10.2"
stack_trace:
dependency: transitive
description:
name: stack_trace
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
url: "https://pub.dev"
source: hosted
version: "1.12.1"
stream_channel:
dependency: transitive
description:
name: stream_channel
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
url: "https://pub.dev"
source: hosted
version: "2.1.4"
string_scanner:
dependency: transitive
description:
name: string_scanner
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
url: "https://pub.dev"
source: hosted
version: "1.4.1"
term_glyph:
dependency: transitive
description:
name: term_glyph
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
url: "https://pub.dev"
source: hosted
version: "1.2.2"
test_api:
dependency: transitive
description:
name: test_api
sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55
url: "https://pub.dev"
source: hosted
version: "0.7.7"
vector_math:
dependency: transitive
description:
name: vector_math
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
url: "https://pub.dev"
source: hosted
version: "2.2.0"
vm_service:
dependency: transitive
description:
name: vm_service
sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60"
url: "https://pub.dev"
source: hosted
version: "15.0.2"
web:
dependency: transitive
description:
name: web
sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a"
url: "https://pub.dev"
source: hosted
version: "1.1.1"
sdks:
dart: ">=3.10.7 <4.0.0"
flutter: ">=3.22.0"

View File

@ -0,0 +1,56 @@
name: monitoring_repository
description: 'A new Flutter package project.'
version: 0.0.1
homepage:
environment:
sdk: ^3.10.7
flutter: '>=1.17.0'
dependencies:
flutter:
sdk: flutter
rxdart: ^0.27.7
firebase_database: ^12.1.3
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^6.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# To add assets to your package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package

View File

@ -432,6 +432,13 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.17.0"
monitoring_repository:
dependency: "direct main"
description:
path: "packages/monitoring_repository"
relative: true
source: path
version: "0.0.1"
native_toolchain_c:
dependency: transitive
description:
@ -669,5 +676,5 @@ packages:
source: hosted
version: "3.1.3"
sdks:
dart: ">=3.10.3 <4.0.0"
dart: ">=3.10.7 <4.0.0"
flutter: ">=3.38.4"

View File

@ -1,5 +1,5 @@
name: klimatologiot
description: "A new Flutter project."
description: 'A new Flutter project.'
publish_to: 'none'
version: 1.0.0+1
environment:
@ -22,6 +22,8 @@ dependencies:
rxdart: ^0.27.7
user_repository:
path: packages/user_repository
monitoring_repository:
path: packages/monitoring_repository
dev_dependencies:
flutter_lints: ^3.0.0

View File

@ -5,15 +5,80 @@
// gestures. You can also use WidgetTester to find child widgets in the widget
// tree, read text, and verify that the values of widget properties are correct.
import 'package:firebase_database/firebase_database.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
import 'package:user_repository/user_repository.dart';
import '../lib/app.dart';
import 'package:klimatologiot/main.dart';
// import 'package:klimatologiot/main.dart';
class FakeUserRepository implements UserRepository {
@override
Stream<MyUser?> get user => const Stream.empty();
@override
Future<void> logOut() async {}
@override
Future<void> setUserData(MyUser user) {
// TODO: implement setUserData
throw UnimplementedError();
}
@override
Future<void> signIn(String email, String password) {
// TODO: implement signIn
throw UnimplementedError();
}
@override
Future<void> signInWithGoogle() {
// TODO: implement signInWithGoogle
throw UnimplementedError();
}
@override
Future<MyUser> signUp(MyUser myUser, String password) {
// TODO: implement signUp
throw UnimplementedError();
}
// tambahin kalau ada method lain
}
class FakeMonitoringRepository implements MonitoringRepository {
get _db => null;
// Satu fungsi untuk semua jenis sensor
// Kamu cukup masukkan "path" database-nya saja
@override
// Jika ingin mengambil data sekali saja (bukan stream)
Future<DataSnapshot> getSensorSnapshot(String path) async {
return await _db.ref(path).get();
}
@override
Stream<DatabaseEvent> getSensorStream(String path) {
// TODO: implement getSensorStream
throw UnimplementedError();
}
}
void main() {
testWidgets('Counter increments smoke test', (WidgetTester tester) async {
testWidgets('App loads', (WidgetTester tester) async {
final fakeRepo = FakeUserRepository();
final fakeMonitoring = FakeUserRepository();
// Build our app and trigger a frame.
await tester.pumpWidget(const MyApp());
await tester.pumpWidget(
MyApp(fakeRepo, fakeMonitoring as MonitoringRepository),
);
await tester.pump();
expect(find.byType(MaterialApp), findsOneWidget);
// Verify that our counter starts at 0.
expect(find.text('0'), findsOneWidget);