Merge branch 'branch' of https://github.com/kleponijo/klimatologi into branch

This commit is contained in:
Mochamad ongki ramadani 2026-05-05 17:39:25 +07:00
commit 6ef9ab0985
12 changed files with 674 additions and 114 deletions

View File

@ -8,6 +8,7 @@ import '../../monitoring/evaporasi/blocs/evaporasi_bloc.dart';
import '../../monitoring/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart'; import '../../monitoring/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart';
import '../widgets/notification_panel.dart'; import '../widgets/notification_panel.dart';
import '../widgets/sensor_grid.dart'; import '../widgets/sensor_grid.dart';
import '../widgets/dashboard_charts.dart';
import 'main_drawer.dart'; import 'main_drawer.dart';
class HomeScreen extends StatefulWidget { class HomeScreen extends StatefulWidget {
@ -199,6 +200,9 @@ class _HomeBody extends StatelessWidget {
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
const SensorGrid(), const SensorGrid(),
const SizedBox(height: 24), // TAMBAH
const DashboardCharts(), // TAMBAH
const SizedBox(height: 16),
], ],
), ),
); );

View File

@ -0,0 +1,605 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../monitoring/wind_speed/blocs/wind_speed_bloc.dart';
import '../../monitoring/evaporasi/blocs/evaporasi_bloc.dart';
import '../../monitoring/atmospheric_conditions/blocs/atmospheric_conditions_bloc.dart';
/// Period selector khusus untuk dashboard
class DashboardPeriodSelector extends StatelessWidget {
final String selected;
final ValueChanged<String> onChanged;
const DashboardPeriodSelector({
super.key,
required this.selected,
required this.onChanged,
});
static const _periods = ["Hari Ini", "Minggu Ini", "Bulan Ini"];
@override
Widget build(BuildContext context) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: _periods.map((p) {
final isSelected = p == selected;
return Padding(
padding: const EdgeInsets.only(right: 8),
child: GestureDetector(
onTap: () => onChanged(p),
child: AnimatedContainer(
duration: const Duration(milliseconds: 200),
padding:
const EdgeInsets.symmetric(horizontal: 16, vertical: 7),
decoration: BoxDecoration(
color: isSelected ? Colors.blue.shade700 : Colors.white,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isSelected
? Colors.blue.shade700
: Colors.grey.shade300,
),
),
child: Text(
p,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: isSelected ? Colors.white : Colors.grey.shade600,
),
),
),
),
);
}).toList(),
),
);
}
}
/// Widget utama semua grafik dashboard
class DashboardCharts extends StatefulWidget {
const DashboardCharts({super.key});
@override
State<DashboardCharts> createState() => _DashboardChartsState();
}
class _DashboardChartsState extends State<DashboardCharts> {
String _period = "Hari Ini";
void _onPeriodChanged(String p) {
setState(() => _period = p);
context.read<WindSpeedBloc>().add(WindSpeedPeriodChanged(p));
context.read<EvaporasiBloc>().add(EvaporasiPeriodChanged(p));
// AtmosphericBloc tidak punya period (hanya realtime)
}
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header + Period Selector
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Grafik Sensor',
style: Theme.of(context).textTheme.titleMedium?.copyWith(
fontWeight: FontWeight.w600,
color: Colors.grey.shade700,
),
),
],
),
const SizedBox(height: 10),
DashboardPeriodSelector(
selected: _period,
onChanged: _onPeriodChanged,
),
const SizedBox(height: 14),
// Scrollable chart list
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
// 1. Kecepatan Angin
BlocBuilder<WindSpeedBloc, WindSpeedState>(
builder: (context, state) {
final data = switch (_period) {
"Minggu Ini" => state.weeklySpeeds,
"Bulan Ini" => state.monthlySpeeds,
_ => state.dailySpeeds,
};
return _SensorChartCard(
title: 'Kecepatan Angin',
unit: 'm/s',
color: Colors.blue.shade600,
bgColor: Colors.blue.shade50,
icon: Icons.air,
currentValue: state.currentSpeed,
data: data,
period: _period,
isLoading: state.isLoading,
);
},
),
const SizedBox(width: 12),
// 2. Evaporasi
BlocBuilder<EvaporasiBloc, EvaporasiState>(
builder: (context, state) {
final data = switch (_period) {
"Minggu Ini" => state.dailyValues, // weekly jika ada
"Bulan Ini" => state.dailyValues,
_ => state.dailyValues,
};
return _SensorChartCard(
title: 'Evaporasi',
unit: 'mm',
color: Colors.teal.shade600,
bgColor: Colors.teal.shade50,
icon: Icons.water_drop_outlined,
currentValue: state.currentValue,
data: data,
period: _period,
isLoading: state.isLoading,
);
},
),
const SizedBox(width: 12),
// 3. Tekanan Udara hanya realtime (tidak ada history)
BlocBuilder<AtmosphericConditionsBloc,
AtmosphericConditionsState>(
builder: (context, state) {
return _AtmosphericCard(state: state);
},
),
],
),
),
],
);
}
}
//
// Sensor Chart Card untuk angin & evaporasi
//
class _SensorChartCard extends StatelessWidget {
final String title;
final String unit;
final Color color;
final Color bgColor;
final IconData icon;
final double currentValue;
final List<double> data;
final String period;
final bool isLoading;
const _SensorChartCard({
required this.title,
required this.unit,
required this.color,
required this.bgColor,
required this.icon,
required this.currentValue,
required this.data,
required this.period,
required this.isLoading,
});
@override
Widget build(BuildContext context) {
return Container(
width: 260,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
color: bgColor,
borderRadius: BorderRadius.circular(10),
),
child: Icon(icon, color: color, size: 18),
),
const SizedBox(width: 8),
Expanded(
child: Text(
title,
style: const TextStyle(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
],
),
const SizedBox(height: 10),
// Nilai saat ini
if (isLoading)
Container(
height: 28,
width: 80,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(6),
),
)
else
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
currentValue.toStringAsFixed(1),
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: color,
),
),
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(bottom: 3),
child: Text(
unit,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
),
),
),
],
),
const SizedBox(height: 12),
// Mini chart
isLoading
? Container(
height: 70,
decoration: BoxDecoration(
color: Colors.grey.shade100,
borderRadius: BorderRadius.circular(8),
),
)
: _MiniLineChart(
data: data,
color: color,
period: period,
),
const SizedBox(height: 8),
Text(
_periodLabel(period),
style: TextStyle(fontSize: 10, color: Colors.grey.shade400),
),
],
),
);
}
String _periodLabel(String p) {
return switch (p) {
"Minggu Ini" => "Rata-rata per hari minggu ini",
"Bulan Ini" => "Rata-rata per hari bulan ini",
_ => "Rata-rata per jam hari ini",
};
}
}
//
// Atmospheric Card tekanan udara (realtime only, no history)
//
class _AtmosphericCard extends StatelessWidget {
final AtmosphericConditionsState state;
const _AtmosphericCard({required this.state});
@override
Widget build(BuildContext context) {
// Klasifikasi tekanan udara
final (label, labelColor) = _classifyPressure(state.pressure);
return Container(
width: 260,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.06),
blurRadius: 12,
offset: const Offset(0, 4),
),
],
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Header
Row(
children: [
Container(
padding: const EdgeInsets.all(7),
decoration: BoxDecoration(
color: Colors.purple.shade50,
borderRadius: BorderRadius.circular(10),
),
child: Icon(Icons.compress,
color: Colors.purple.shade400, size: 18),
),
const SizedBox(width: 8),
const Expanded(
child: Text(
'Kondisi Atmosfer',
style: TextStyle(fontSize: 13, fontWeight: FontWeight.w600),
),
),
],
),
const SizedBox(height: 10),
// Tekanan utama
if (state.isLoading)
Container(
height: 28,
width: 80,
decoration: BoxDecoration(
color: Colors.grey.shade200,
borderRadius: BorderRadius.circular(6),
),
)
else ...[
Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
state.pressure.toStringAsFixed(1),
style: TextStyle(
fontSize: 26,
fontWeight: FontWeight.bold,
color: Colors.purple.shade400,
),
),
const SizedBox(width: 4),
Padding(
padding: const EdgeInsets.only(bottom: 3),
child: Text(
'hPa',
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
),
),
const Spacer(),
Container(
padding:
const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: labelColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: labelColor,
),
),
),
],
),
const SizedBox(height: 12),
// Info tambahan: suhu, humidity, altitude
_AtmosInfoRow(
items: [
_AtmosItem(
Icons.thermostat_rounded,
'${state.temperature.toStringAsFixed(1)}°C',
'Suhu',
Colors.orange.shade400),
_AtmosItem(
Icons.water_drop_rounded,
'${state.humidity.toStringAsFixed(0)}%',
'Kelembapan',
Colors.blue.shade400),
_AtmosItem(
Icons.landscape_rounded,
'${state.altitude.toStringAsFixed(0)} m',
'Altitud',
Colors.green.shade400),
],
),
],
const SizedBox(height: 8),
Text(
'Data realtime — tidak ada history',
style: TextStyle(fontSize: 10, color: Colors.grey.shade400),
),
],
),
);
}
static (String, Color) _classifyPressure(double hpa) {
if (hpa < 1000) return ('Rendah', Colors.orange.shade600);
if (hpa > 1020) return ('Tinggi', Colors.blue.shade600);
return ('Normal', Colors.green.shade600);
}
}
class _AtmosInfoRow extends StatelessWidget {
final List<_AtmosItem> items;
const _AtmosInfoRow({required this.items});
@override
Widget build(BuildContext context) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: items
.map((item) => Expanded(
child: Column(
children: [
Icon(item.icon, color: item.color, size: 18),
const SizedBox(height: 3),
Text(
item.value,
style: const TextStyle(
fontSize: 12, fontWeight: FontWeight.bold),
),
Text(
item.label,
style:
TextStyle(fontSize: 10, color: Colors.grey.shade500),
),
],
),
))
.toList(),
);
}
}
class _AtmosItem {
final IconData icon;
final String value;
final String label;
final Color color;
const _AtmosItem(this.icon, this.value, this.label, this.color);
}
//
// Mini Line Chart custom painter, tanpa library tambahan
//
class _MiniLineChart extends StatelessWidget {
final List<double> data;
final Color color;
final String period;
const _MiniLineChart({
required this.data,
required this.color,
required this.period,
});
@override
Widget build(BuildContext context) {
final nonZero = data.where((v) => v > 0).toList();
if (nonZero.isEmpty) {
return SizedBox(
height: 70,
child: Center(
child: Text(
'Belum ada data',
style: TextStyle(fontSize: 11, color: Colors.grey.shade400),
),
),
);
}
return SizedBox(
height: 70,
child: CustomPaint(
painter: _LineChartPainter(data: data, color: color),
size: const Size(double.infinity, 70),
),
);
}
}
class _LineChartPainter extends CustomPainter {
final List<double> data;
final Color color;
const _LineChartPainter({required this.data, required this.color});
@override
void paint(Canvas canvas, Size size) {
if (data.isEmpty) return;
final maxVal = data.reduce((a, b) => a > b ? a : b);
if (maxVal == 0) return;
final points = <Offset>[];
for (int i = 0; i < data.length; i++) {
final x = i / (data.length - 1) * size.width;
final y = size.height - (data[i] / maxVal) * size.height;
points.add(Offset(x, y));
}
// Area fill (gradient)
final path = Path();
path.moveTo(points.first.dx, size.height);
for (final p in points) {
path.lineTo(p.dx, p.dy);
}
path.lineTo(points.last.dx, size.height);
path.close();
final fillPaint = Paint()
..shader = LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [color.withValues(alpha: 0.25), color.withValues(alpha: 0.0)],
).createShader(Rect.fromLTWH(0, 0, size.width, size.height));
canvas.drawPath(path, fillPaint);
// Line
final linePaint = Paint()
..color = color
..strokeWidth = 2
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round;
final linePath = Path();
linePath.moveTo(points.first.dx, points.first.dy);
for (int i = 1; i < points.length; i++) {
linePath.lineTo(points[i].dx, points[i].dy);
}
canvas.drawPath(linePath, linePaint);
// Dot di titik terakhir
final lastPoint = points.last;
canvas.drawCircle(
lastPoint,
3.5,
Paint()..color = color,
);
canvas.drawCircle(
lastPoint,
3.5,
Paint()
..color = Colors.white
..style = PaintingStyle.stroke
..strokeWidth = 1.5,
);
}
@override
bool shouldRepaint(_LineChartPainter old) =>
old.data != data || old.color != color;
}

View File

@ -50,68 +50,6 @@ class SensorGrid extends StatelessWidget {
), ),
), ),
// --- SUHU AIR (dari Evaporasi) ---
BlocBuilder<EvaporasiBloc, EvaporasiState>(
builder: (context, state) => SensorCard(
width: cardWidth,
icon: Icons.thermostat_outlined,
iconColor: Colors.orange.shade600,
iconBgColor: Colors.orange.shade50,
label: 'Suhu Air',
value: state.isLoading
? ''
: state.temperature.toStringAsFixed(1),
unit: '°C',
isLoading: state.isLoading,
),
),
// --- TINGGI AIR (dari Evaporasi) ---
BlocBuilder<EvaporasiBloc, EvaporasiState>(
builder: (context, state) => SensorCard(
width: cardWidth,
icon: Icons.straighten_outlined,
iconColor: Colors.cyan.shade600,
iconBgColor: Colors.cyan.shade50,
label: 'Tinggi Air',
value:
state.isLoading ? '' : state.waterLevel.toStringAsFixed(1),
unit: 'cm',
isLoading: state.isLoading,
),
),
// --- SUHU UDARA (dari Atmospheric) ---
BlocBuilder<AtmosphericConditionsBloc, AtmosphericConditionsState>(
builder: (context, state) => SensorCard(
width: cardWidth,
icon: Icons.device_thermostat,
iconColor: Colors.red.shade400,
iconBgColor: Colors.red.shade50,
label: 'Suhu Udara',
value: state.isLoading
? ''
: state.temperature.toStringAsFixed(1),
unit: '°C',
isLoading: state.isLoading,
),
),
// --- KELEMBAPAN ---
BlocBuilder<AtmosphericConditionsBloc, AtmosphericConditionsState>(
builder: (context, state) => SensorCard(
width: cardWidth,
icon: Icons.water_outlined,
iconColor: Colors.indigo.shade400,
iconBgColor: Colors.indigo.shade50,
label: 'Kelembapan',
value:
state.isLoading ? '' : state.humidity.toStringAsFixed(1),
unit: '%',
isLoading: state.isLoading,
),
),
// --- TEKANAN UDARA --- // --- TEKANAN UDARA ---
BlocBuilder<AtmosphericConditionsBloc, AtmosphericConditionsState>( BlocBuilder<AtmosphericConditionsBloc, AtmosphericConditionsState>(
builder: (context, state) => SensorCard( builder: (context, state) => SensorCard(
@ -126,21 +64,6 @@ class SensorGrid extends StatelessWidget {
isLoading: state.isLoading, isLoading: state.isLoading,
), ),
), ),
// --- KETINGGIAN ---
BlocBuilder<AtmosphericConditionsBloc, AtmosphericConditionsState>(
builder: (context, state) => SensorCard(
width: cardWidth,
icon: Icons.terrain_outlined,
iconColor: Colors.green.shade600,
iconBgColor: Colors.green.shade50,
label: 'Ketinggian',
value:
state.isLoading ? '' : state.altitude.toStringAsFixed(1),
unit: 'm',
isLoading: state.isLoading,
),
),
], ],
); );
}, },
@ -181,7 +104,7 @@ class SensorCard extends StatelessWidget {
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
boxShadow: [ boxShadow: [
BoxShadow( BoxShadow(
color: Colors.black.withOpacity(0.05), color: Colors.black.withValues(alpha: 0.05),
blurRadius: 8, blurRadius: 8,
offset: const Offset(0, 2), offset: const Offset(0, 2),
), ),

View File

@ -115,7 +115,8 @@ class DeviceSetupBloc extends Bloc<DeviceSetupEvent, DeviceSetupState> {
// ESP restart setelah terima kredensial koneksi putus itu normal! // ESP restart setelah terima kredensial koneksi putus itu normal!
// DioException di sini kemungkinan besar karena ESP sudah restart. // DioException di sini kemungkinan besar karena ESP sudah restart.
if (e.type == DioExceptionType.receiveTimeout || if (e.type == DioExceptionType.receiveTimeout ||
e.type == DioExceptionType.connectionError) { e.type == DioExceptionType.connectionError ||
e.type == DioExceptionType.sendTimeout) {
emit(state.copyWith( emit(state.copyWith(
status: DeviceSetupStatus.success, status: DeviceSetupStatus.success,
successMessage: successMessage:

View File

@ -1,6 +1,7 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import '../blocs/device_setup_bloc.dart'; import '../blocs/device_setup_bloc.dart';
import 'package:app_settings/app_settings.dart';
class DeviceSetupScreen extends StatefulWidget { class DeviceSetupScreen extends StatefulWidget {
const DeviceSetupScreen({super.key}); const DeviceSetupScreen({super.key});
@ -159,7 +160,7 @@ class _DeviceSetupScreenState extends State<DeviceSetupScreen> {
showDialog( showDialog(
context: context, context: context,
barrierDismissible: false, barrierDismissible: false,
builder: (_) => AlertDialog( builder: (dialogContext) => AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Row( title: Row(
children: [ children: [
@ -173,12 +174,21 @@ class _DeviceSetupScreenState extends State<DeviceSetupScreen> {
), ),
content: Text(message), content: Text(message),
actions: [ actions: [
if (!success) // tambah tombol Coba Lagi jika gagal
TextButton(
onPressed: () {
Navigator.of(dialogContext).pop(); // tutup dialog
context.read<DeviceSetupBloc>().add(
ResetDeviceSetupEvent()); // kembali ke wind_speed_screen
},
child: const Text('Coba Lagi'),
),
TextButton( TextButton(
onPressed: () { onPressed: () {
Navigator.of(context).pop(); // tutup dialog Navigator.of(dialogContext).pop();
Navigator.of(context).pop(); // kembali ke wind_speed_screen Navigator.of(context).pop();
}, },
child: const Text('Selesai'), child: Text(success ? 'Selesai' : 'Tutup'),
), ),
], ],
), ),
@ -353,13 +363,7 @@ class _OpenWifiSettingsButton extends StatelessWidget {
// Buka pengaturan WiFi sistem HP // Buka pengaturan WiFi sistem HP
// Butuh package: app_settings (opsional) // Butuh package: app_settings (opsional)
// AppSettings.openWIFISettings(); // AppSettings.openWIFISettings();
ScaffoldMessenger.of(context).showSnackBar( AppSettings.openAppSettings(type: AppSettingsType.wifi);
const SnackBar(
content: Text(
'Buka Pengaturan WiFi HP → sambungkan ke "Anemometer-Setup"'),
behavior: SnackBarBehavior.floating,
),
);
}, },
icon: const Icon(Icons.settings_rounded, size: 16), icon: const Icon(Icons.settings_rounded, size: 16),
label: const Text('Buka Setting'), label: const Text('Buka Setting'),

View File

@ -11,8 +11,8 @@ part 'wind_speed_state.dart';
/// Threshold kecepatan angin (satuan m/s) /// Threshold kecepatan angin (satuan m/s)
/// Referensi: Beaufort scale & BMKG /// Referensi: Beaufort scale & BMKG
const double _kWindWarning = 2.0; // Waspada: 812.5 m/s (~2945 km/h) const double _kWindWarning = 8.0; // Waspada: 812.5 m/s (~2945 km/h)
const double _kWindDanger = 4.0; // Bahaya: > 12.5 m/s (> 45 km/h) const double _kWindDanger = 12.5; // Bahaya: > 12.5 m/s (> 45 km/h)
class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> { class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
final MonitoringRepository _repository; final MonitoringRepository _repository;
@ -50,15 +50,8 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
(json) => MyWindSpeed.fromJson(json), (json) => MyWindSpeed.fromJson(json),
); );
/// 2. Mapping default (harian) // Ini saja yang benar
final raw = TimeSeriesMapper.toDaily( final dailyGraph = TimeSeriesMapper.smooth(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.speed,
);
final dailyGraph = TimeSeriesMapper.smooth(raw);
final daily = TimeSeriesMapper.smooth(
TimeSeriesMapper.toDaily( TimeSeriesMapper.toDaily(
data: history, data: history,
getTime: (e) => e.timestamp, getTime: (e) => e.timestamp,
@ -93,6 +86,9 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
weeklySpeeds: weekly, weeklySpeeds: weekly,
monthlySpeeds: monthly, monthlySpeeds: monthly,
isLoading: false, isLoading: false,
alertLevel: history.isNotEmpty // tambah ini
? _getAlertLevel(history.last.speed)
: "Normal",
)); ));
/// 3. Start realtime stream (manual subscription) /// 3. Start realtime stream (manual subscription)
@ -138,6 +134,7 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
emit(state.copyWith( emit(state.copyWith(
currentSpeed: newValue, currentSpeed: newValue,
dailySpeeds: updated, dailySpeeds: updated,
alertLevel: _getAlertLevel(newValue),
)); ));
} }
@ -194,6 +191,12 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
return super.close(); return super.close();
} }
String _getAlertLevel(double speed) {
if (speed >= _kWindDanger) return "Bahaya";
if (speed >= _kWindWarning) return "Waspada";
return "Normal";
}
// ========================= // =========================
// HELPER: kirim alert ke NotificationBloc // HELPER: kirim alert ke NotificationBloc
// ========================= // =========================

View File

@ -14,11 +14,3 @@ class WindSpeedPeriodChanged extends WindSpeedEvent {
final String period; final String period;
const WindSpeedPeriodChanged(this.period); const WindSpeedPeriodChanged(this.period);
} }
// Event internal: saat data baru masuk dari Firebase
class _WindSpeedUpdated extends WindSpeedEvent {
final MyWindSpeed data;
const _WindSpeedUpdated(this.data);
@override
List<Object> get props => [data];
}

View File

@ -8,6 +8,7 @@ class WindSpeedState extends Equatable {
final List<double> monthlySpeeds; final List<double> monthlySpeeds;
final bool isLoading; final bool isLoading;
final List<MyWindSpeed> history; final List<MyWindSpeed> history;
final String alertLevel; // "Normal" | "Waspada" | "Bahaya"
const WindSpeedState({ const WindSpeedState({
this.currentSpeed = 0.0, this.currentSpeed = 0.0,
@ -17,6 +18,7 @@ class WindSpeedState extends Equatable {
this.history = const [], this.history = const [],
this.monthlySpeeds = const [], this.monthlySpeeds = const [],
this.weeklySpeeds = const [], this.weeklySpeeds = const [],
this.alertLevel = "Normal",
}); });
WindSpeedState copyWith({ WindSpeedState copyWith({
@ -27,6 +29,7 @@ class WindSpeedState extends Equatable {
List<double>? monthlySpeeds, List<double>? monthlySpeeds,
bool? isLoading, bool? isLoading,
List<MyWindSpeed>? history, List<MyWindSpeed>? history,
String? alertLevel,
}) { }) {
return WindSpeedState( return WindSpeedState(
currentSpeed: currentSpeed ?? this.currentSpeed, currentSpeed: currentSpeed ?? this.currentSpeed,
@ -36,10 +39,19 @@ class WindSpeedState extends Equatable {
monthlySpeeds: monthlySpeeds ?? this.monthlySpeeds, monthlySpeeds: monthlySpeeds ?? this.monthlySpeeds,
isLoading: isLoading ?? this.isLoading, isLoading: isLoading ?? this.isLoading,
history: history ?? this.history, history: history ?? this.history,
alertLevel: alertLevel ?? this.alertLevel,
); );
} }
@override @override
List<Object> get props => List<Object> get props => [
[currentSpeed, selectedPeriod, dailySpeeds, isLoading, history]; currentSpeed,
selectedPeriod,
dailySpeeds,
weeklySpeeds,
monthlySpeeds,
isLoading,
history,
alertLevel,
];
} }

View File

@ -85,7 +85,7 @@ class WindSpeedScreen extends StatelessWidget {
const SizedBox(height: 30), const SizedBox(height: 30),
// 3. Info Tambahan (Status/Periode) // 3. Info Tambahan (Status/Periode)
_buildDetailRow(state.selectedPeriod), _buildDetailRow(state.selectedPeriod, state.alertLevel),
const SizedBox(height: 24), const SizedBox(height: 24),
// Tombol Export PDF // Tombol Export PDF
@ -143,11 +143,16 @@ class WindSpeedScreen extends StatelessWidget {
); );
} }
Widget _buildDetailRow(String period) { Widget _buildDetailRow(String period, String alertLevel) {
final (icon, color) = switch (alertLevel) {
"Bahaya" => (Icons.warning_rounded, Colors.red),
"Waspada" => (Icons.info_outline, Colors.orange),
_ => (Icons.check_circle, Colors.green),
};
return Row( return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween, mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [ children: [
_miniInfoCard("Status", "Normal", Icons.check_circle, Colors.green), _miniInfoCard("Status", alertLevel, icon, color),
_miniInfoCard("Periode", period, Icons.timer, Colors.orange), _miniInfoCard("Periode", period, Icons.timer, Colors.orange),
], ],
); );

View File

@ -5,6 +5,7 @@
import FlutterMacOS import FlutterMacOS
import Foundation import Foundation
import app_settings
import cloud_firestore import cloud_firestore
import firebase_auth import firebase_auth
import firebase_core import firebase_core
@ -13,6 +14,7 @@ import google_sign_in_ios
import printing import printing
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AppSettingsPlugin.register(with: registry.registrar(forPlugin: "AppSettingsPlugin"))
FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin")) FLTFirebaseFirestorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseFirestorePlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))

View File

@ -9,6 +9,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.66" version: "1.3.66"
app_settings:
dependency: "direct main"
description:
name: app_settings
sha256: "64d50e666fd96ae90301bf71205f05019286f940ad6f5fed3d1be19c6af7546a"
url: "https://pub.dev"
source: hosted
version: "7.0.0"
archive: archive:
dependency: transitive dependency: transitive
description: description:

View File

@ -6,6 +6,7 @@ environment:
sdk: ^3.1.0 sdk: ^3.1.0
dependencies: dependencies:
app_settings: ^7.0.0
bloc: ^8.1.0 bloc: ^8.1.0
bloc_concurrency: ^0.2.5 bloc_concurrency: ^0.2.5
cloud_firestore: ^6.1.1 cloud_firestore: ^6.1.1