joss
This commit is contained in:
parent
95b1759344
commit
fd75feb34f
Binary file not shown.
|
After Width: | Height: | Size: 63 KiB |
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
@ -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 {}
|
||||
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
|
|
@ -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),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
|
@ -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
|
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,3 +1,3 @@
|
|||
export 'wind_speed.dart';
|
||||
export 'evaporasi.dart';
|
||||
export 'pressure_sensor.dart';
|
||||
export 'atmospheric_conditions.dart';
|
||||
|
|
|
|||
Loading…
Reference in New Issue