This commit is contained in:
kleponijo 2026-04-19 12:44:37 +07:00
parent 95b1759344
commit fd75feb34f
14 changed files with 586 additions and 1051 deletions

BIN
images/logo_klimatologi.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

View File

@ -1,9 +1,13 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:klimatologiot/screens/monitoring/evaporasi/blocs/evaporasi_bloc.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
import '../../../blocs/authentication_bloc/authentication_bloc.dart';
import '../../monitoring/wind_speed/views/wind_speed_screen.dart';
import '../../monitoring/wind_speed/blocs/wind_speed_bloc.dart';
import '../../monitoring/evaporasi/views/evaporasi_screen.dart';
import '../../monitoring/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart';
import '../../monitoring/atmospheric_conditions/views/atmospheric_screen.dart';
class MainDrawer extends StatelessWidget {
const MainDrawer({super.key});
@ -68,7 +72,20 @@ class MainDrawer extends StatelessWidget {
ListTile(
leading: const Icon(Icons.water_drop),
title: const Text("Evaporasi"),
onTap: () {},
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider<EvaporasiBloc>(
create: (context) => EvaporasiBloc(
repository: context.read<MonitoringRepository>(),
)..add(WatchEvaporasiStarted()),
child: const EvaporasiScreen(),
),
),
);
},
),
ListTile(
leading: Image.asset(
@ -76,8 +93,21 @@ class MainDrawer extends StatelessWidget {
height: 20,
width: 20,
),
title: const Text("Tekanan Udara"),
onTap: () {},
title: const Text("Kondisi Atmosfer"),
onTap: () {
Navigator.pop(context);
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => BlocProvider<AtmosphericConditionsBloc>(
create: (context) => AtmosphericConditionsBloc(
repository: context.read<MonitoringRepository>(),
)..add(WatchAtmosphericConditionsStarted()),
child: AtmosphericScreen(),
),
),
);
},
),
const Spacer(), // Dorong menu logout ke paling bawah
const Divider(),

View File

@ -0,0 +1,69 @@
import 'dart:async';
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
part 'atmospheric_conditions_event.dart';
part 'atmospheric_conditions_state.dart';
class AtmosphericConditionsBloc
extends Bloc<AtmosphericConditionsEvent, AtmosphericConditionsState> {
final MonitoringRepository _repository;
StreamSubscription<AtmosphericConditions>? _subscription;
AtmosphericConditionsBloc({required MonitoringRepository repository})
: _repository = repository,
super(const AtmosphericConditionsState()) {
on<WatchAtmosphericConditionsStarted>(_onStarted);
on<_AtmosphericConditionsUpdated>(_onUpdated);
}
/// 🚀 START
Future<void> _onStarted(
WatchAtmosphericConditionsStarted event,
Emitter<AtmosphericConditionsState> emit,
) async {
emit(state.copyWith(isLoading: true));
await _subscription?.cancel();
_subscription = _repository
.getSensorStream(
'sensor/latest',
(json) => AtmosphericConditions.fromJson(json),
)
.listen((data) {
add(_AtmosphericConditionsUpdated(data));
});
}
/// REALTIME UPDATE
void _onUpdated(
_AtmosphericConditionsUpdated event,
Emitter<AtmosphericConditionsState> emit,
) {
emit(state.copyWith(
temperature: event.data.temperature,
humidity: event.data.humidity,
pressure: event.data.pressure,
altitude: event.data.altitude,
isLoading: false,
));
}
@override
Future<void> close() async {
await _subscription?.cancel();
return super.close();
}
}
/// INTERNAL EVENT
class _AtmosphericConditionsUpdated extends AtmosphericConditionsEvent {
final AtmosphericConditions data;
const _AtmosphericConditionsUpdated(this.data);
@override
List<Object> get props => [data];
}

View File

@ -0,0 +1,10 @@
part of 'atmospheric_conditions_bloc.dart';
abstract class AtmosphericConditionsEvent extends Equatable {
const AtmosphericConditionsEvent();
@override
List<Object> get props => [];
}
class WatchAtmosphericConditionsStarted extends AtmosphericConditionsEvent {}

View File

@ -0,0 +1,43 @@
part of 'atmospheric_conditions_bloc.dart';
class AtmosphericConditionsState extends Equatable {
final double temperature;
final double humidity;
final double pressure;
final double altitude;
final bool isLoading;
const AtmosphericConditionsState({
this.temperature = 0.0,
this.humidity = 0.0,
this.pressure = 0.0,
this.altitude = 0.0,
this.isLoading = true,
});
AtmosphericConditionsState copyWith({
double? temperature,
double? humidity,
double? pressure,
double? altitude,
bool? isLoading,
}) {
return AtmosphericConditionsState(
temperature: temperature ?? this.temperature,
humidity: humidity ?? this.humidity,
pressure: pressure ?? this.pressure,
altitude: altitude ?? this.altitude,
isLoading: isLoading ?? this.isLoading,
);
}
@override
List<Object> get props => [
temperature,
humidity,
pressure,
altitude,
isLoading,
];
}

View File

@ -0,0 +1,150 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/atmospheric_conditions_bloc.dart';
class AtmosphericScreen extends StatelessWidget {
const AtmosphericScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.grey.shade100,
appBar: AppBar(
title: const Text(
"Kondisi Atmosfer",
style: TextStyle(fontWeight: FontWeight.bold),
),
centerTitle: true,
backgroundColor: Colors.transparent,
elevation: 0,
foregroundColor: Colors.black,
),
body: BlocBuilder<AtmosphericConditionsBloc, AtmosphericConditionsState>(
builder: (context, state) {
if (state.isLoading) {
return const Center(child: CircularProgressIndicator());
}
return SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
children: [
_mainTemperature(state),
const SizedBox(height: 25),
_gridInfo(state),
],
),
);
},
),
);
}
/// =========================
/// 🌡 TEMPERATURE (HERO CARD)
/// =========================
Widget _mainTemperature(AtmosphericConditionsState state) {
return Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 40),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
const Color.fromARGB(255, 38, 255, 222),
const Color.fromARGB(255, 53, 132, 229)
],
),
borderRadius: BorderRadius.circular(25),
boxShadow: [
BoxShadow(
color: const Color.fromARGB(255, 0, 191, 255).withOpacity(0.3),
blurRadius: 20,
offset: const Offset(0, 10),
)
],
),
child: Column(
children: [
const Icon(Icons.thermostat, color: Colors.white, size: 50),
const SizedBox(height: 10),
Text(
state.temperature.toStringAsFixed(1),
style: const TextStyle(
fontSize: 70,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const Text(
"°C",
style: TextStyle(color: Colors.white70, fontSize: 18),
),
],
),
);
}
/// =========================
/// 📊 INFO GRID
/// =========================
Widget _gridInfo(AtmosphericConditionsState state) {
return GridView.count(
crossAxisCount: 2,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
mainAxisSpacing: 15,
crossAxisSpacing: 15,
childAspectRatio: 1.6,
children: [
_infoCard(
"Kelembapan",
"${state.humidity.toStringAsFixed(1)} %",
Icons.water_drop,
Colors.blue,
),
_infoCard(
"Tekanan",
"${state.pressure.toStringAsFixed(1)} hPa",
Icons.speed,
Colors.green,
),
_infoCard(
"Ketinggian",
"${state.altitude.toStringAsFixed(1)} m",
Icons.terrain,
Colors.brown,
),
],
);
}
Widget _infoCard(String title, String value, IconData icon, Color color) {
return Container(
padding: const EdgeInsets.all(15),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
),
child: Row(
children: [
Icon(icon, color: color, size: 30),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
title,
style: const TextStyle(color: Colors.grey, fontSize: 12),
),
Text(
value,
style: const TextStyle(fontWeight: FontWeight.bold),
),
],
)
],
),
);
}
}

View File

@ -3,7 +3,7 @@ import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
import '../../../core/utils/time_series_mapper.dart';
import '../../../../core/utils/time_series_mapper.dart';
part 'evaporasi_event.dart';
part 'evaporasi_state.dart';
@ -15,7 +15,6 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
EvaporasiBloc({required MonitoringRepository repository})
: _repository = repository,
super(const EvaporasiState()) {
on<WatchEvaporasiStarted>(_onStarted);
on<_EvaporasiRealtimeUpdated>(_onRealtimeUpdated);
on<EvaporasiPeriodChanged>(_onPeriodChanged);
@ -25,10 +24,9 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
/// 🚀 START
/// =========================
Future<void> _onStarted(
WatchEvaporasiStarted event,
Emitter<EvaporasiState> emit,
) async {
WatchEvaporasiStarted event,
Emitter<EvaporasiState> emit,
) async {
emit(state.copyWith(isLoading: true));
final history = await _repository.getSensorHistory(
@ -39,7 +37,7 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
final dailyGraph = TimeSeriesMapper.toDaily(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.value, // sesuaikan nama field
getValue: (e) => e.evaporasi, // sesuaikan nama field
);
emit(state.copyWith(
@ -52,7 +50,7 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
_subscription = _repository
.getSensorStream(
'evaporasi/realtime',
'kelompok1/evaporasi',
(json) => Evaporasi.fromJson(json),
)
.listen((data) {
@ -64,20 +62,21 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
/// REALTIME
/// =========================
void _onRealtimeUpdated(
_EvaporasiRealtimeUpdated event,
Emitter<EvaporasiState> emit,
) {
_EvaporasiRealtimeUpdated event,
Emitter<EvaporasiState> emit,
) {
final updated = List<double>.from(state.dailyValues);
final index = DateTime.now().hour;
if (index < updated.length) {
updated[index] = event.data.value; // sesuaikan field
updated[index] = event.data.evaporasi; // sesuaikan field
}
emit(state.copyWith(
currentValue: event.data.value,
currentValue: event.data.evaporasi,
temperature: event.data.suhu,
waterLevel: event.data.tinggiAir,
dailyValues: updated,
));
}
@ -86,10 +85,9 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
/// 📊 PERIOD
/// =========================
Future<void> _onPeriodChanged(
EvaporasiPeriodChanged event,
Emitter<EvaporasiState> emit,
) async {
EvaporasiPeriodChanged event,
Emitter<EvaporasiState> emit,
) async {
emit(state.copyWith(isLoading: true, selectedPeriod: event.period));
final history = state.history;
@ -100,19 +98,19 @@ class EvaporasiBloc extends Bloc<EvaporasiEvent, EvaporasiState> {
updated = TimeSeriesMapper.toWeekly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.value,
getValue: (e) => e.evaporasi,
);
} else if (event.period == "Bulan Ini") {
updated = TimeSeriesMapper.toMonthly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.value,
getValue: (e) => e.evaporasi,
);
} else {
updated = TimeSeriesMapper.toDaily(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.value,
getValue: (e) => e.evaporasi,
);
}
@ -137,4 +135,4 @@ class _EvaporasiRealtimeUpdated extends EvaporasiEvent {
@override
List<Object> get props => [data];
}
}

View File

@ -0,0 +1,21 @@
part of 'evaporasi_bloc.dart';
abstract class EvaporasiEvent extends Equatable {
const EvaporasiEvent();
@override
List<Object> get props => [];
}
/// 🚀 START MONITORING
class WatchEvaporasiStarted extends EvaporasiEvent {}
/// 📊 GANTI PERIODE (Harian / Mingguan / Bulanan)
class EvaporasiPeriodChanged extends EvaporasiEvent {
final String period;
const EvaporasiPeriodChanged(this.period);
@override
List<Object> get props => [period];
}

View File

@ -0,0 +1,54 @@
part of 'evaporasi_bloc.dart';
class EvaporasiState extends Equatable {
final double currentValue; // nilai evaporasi realtime
final double temperature; // suhu (opsional dari firebase)
final double waterLevel; // tinggi air
final String selectedPeriod;
final List<double> dailyValues; // untuk grafik
final List<Evaporasi> history;
final bool isLoading;
const EvaporasiState({
this.currentValue = 0.0,
this.temperature = 0.0,
this.waterLevel = 0.0,
this.selectedPeriod = "Hari Ini",
this.dailyValues = const [],
this.history = const [],
this.isLoading = true,
});
EvaporasiState copyWith({
double? currentValue,
double? temperature,
double? waterLevel,
String? selectedPeriod,
List<double>? dailyValues,
List<Evaporasi>? history,
bool? isLoading,
}) {
return EvaporasiState(
currentValue: currentValue ?? this.currentValue,
temperature: temperature ?? this.temperature,
waterLevel: waterLevel ?? this.waterLevel,
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
dailyValues: dailyValues ?? this.dailyValues,
history: history ?? this.history,
isLoading: isLoading ?? this.isLoading,
);
}
@override
List<Object> get props => [
currentValue,
temperature,
waterLevel,
selectedPeriod,
dailyValues,
history,
isLoading,
];
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,25 @@
class AtmosphericConditions {
final double temperature;
final double humidity;
final double pressure;
final double altitude;
final DateTime timestamp;
AtmosphericConditions({
required this.temperature,
required this.humidity,
required this.pressure,
required this.altitude,
required this.timestamp,
});
factory AtmosphericConditions.fromJson(Map<dynamic, dynamic> json) {
return AtmosphericConditions(
temperature: (json['temperature'] ?? 0).toDouble(),
humidity: (json['humidity'] ?? 0).toDouble(),
pressure: (json['pressure'] ?? 0).toDouble(),
altitude: (json['altitude'] ?? 0).toDouble(),
timestamp: DateTime.now(), // karena kamu pakai latest
);
}
}

View File

@ -0,0 +1,36 @@
class Evaporasi {
final double evaporasi;
final double suhu;
final double tinggiAir;
final DateTime timestamp;
Evaporasi({
required this.evaporasi,
required this.suhu,
required this.tinggiAir,
required this.timestamp,
});
static final empty = Evaporasi(
evaporasi: 0.0,
suhu: 0.0,
tinggiAir: 0.0,
timestamp: DateTime.fromMillisecondsSinceEpoch(0),
);
factory Evaporasi.fromJson(Map<dynamic, dynamic> json) {
final int jam = (json['jam'] ?? 0) as int;
final int menit = (json['menit'] ?? 0) as int;
final now = DateTime.now();
return Evaporasi(
evaporasi: (json['evaporasi'] ?? 0).toDouble(),
suhu: (json['suhu'] ?? 0).toDouble(),
tinggiAir: (json['tinggi_air'] ?? 0).toDouble(),
/// 🔥 bikin timestamp dari jam & menit
timestamp: DateTime(now.year, now.month, now.day, jam, menit),
);
}
}

View File

@ -1,3 +1,3 @@
export 'wind_speed.dart';
export 'evaporasi.dart';
export 'pressure_sensor.dart';
export 'atmospheric_conditions.dart';