oke
This commit is contained in:
parent
74a1609c21
commit
a6e74d4da0
|
|
@ -49,7 +49,11 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
|||
WatchWindSpeedStarted event,
|
||||
Emitter<WindSpeedState> emit,
|
||||
) async {
|
||||
emit(state.copyWith(isLoading: true));
|
||||
// Cancel stream lama dulu sebelum apapun
|
||||
await _subscription?.cancel();
|
||||
_subscription = null;
|
||||
|
||||
emit(state.copyWith(isLoading: true, currentSpeed: 0));
|
||||
final deviceId = await _getDeviceId();
|
||||
|
||||
final historyMap = await _repository.getSensorHistoryWithKeys(
|
||||
|
|
@ -74,7 +78,7 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
|||
history.isNotEmpty ? _getAlertLevel(history.last.speed) : 'Normal',
|
||||
));
|
||||
|
||||
await _subscription?.cancel();
|
||||
// Subscribe stream baru
|
||||
_subscription = _repository
|
||||
.getSensorStream(
|
||||
'anemometer/$deviceId/realtime',
|
||||
|
|
@ -90,6 +94,15 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
|||
_WindSpeedRealtimeUpdated event,
|
||||
Emitter<WindSpeedState> emit,
|
||||
) {
|
||||
// Guard: abaikan data jika windweg 0 dan pulse 0 (ESP tidak kirim data nyata)
|
||||
if (event.data.windwegKm == 0 && event.data.totalPulse == 0) {
|
||||
emit(state.copyWith(
|
||||
currentSpeed: 0,
|
||||
alertLevel: 'Normal',
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
final updated = List<double>.from(state.dailySpeeds);
|
||||
final index = DateTime.now().hour;
|
||||
double newValue = event.data.speed;
|
||||
|
|
|
|||
|
|
@ -747,7 +747,7 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
|
|||
const Icon(Icons.air, color: Colors.white, size: 46),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
speed.toStringAsFixed(1),
|
||||
speed.toStringAsFixed(2),
|
||||
style: const TextStyle(
|
||||
fontSize: 80,
|
||||
fontWeight: FontWeight.bold,
|
||||
|
|
@ -755,11 +755,11 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
|
|||
height: 1,
|
||||
),
|
||||
),
|
||||
const Text('m/s',
|
||||
const Text('km/h',
|
||||
style: TextStyle(fontSize: 20, color: Colors.white70)),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${(speed * 3.6).toStringAsFixed(1)} km/h',
|
||||
'${(speed / 3.6).toStringAsFixed(3)} m/s',
|
||||
style: const TextStyle(fontSize: 14, color: Colors.white54),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
|
|
@ -1056,8 +1056,8 @@ class _HistoryTile extends StatelessWidget {
|
|||
const _HistoryTile({required this.item, required this.index});
|
||||
|
||||
String get _alertLevel {
|
||||
if (item.speed >= 12.5) return 'Bahaya';
|
||||
if (item.speed >= 8.0) return 'Waspada';
|
||||
if (item.speed >= 45.0) return 'Bahaya';
|
||||
if (item.speed >= 25.5) return 'Waspada';
|
||||
return 'Normal';
|
||||
}
|
||||
|
||||
|
|
@ -1085,98 +1085,188 @@ class _HistoryTile extends StatelessWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final kmh = item.speed * 3.6;
|
||||
final bg = index.isEven ? Colors.grey.shade50 : Colors.white;
|
||||
final hasTotal = item.totalWindwegKm > 0 || item.totalPulse > 0;
|
||||
|
||||
return Container(
|
||||
color: bg,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
child: Row(
|
||||
// ← child pakai Column, bukan Row langsung
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Nomor
|
||||
SizedBox(
|
||||
width: 28,
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade400,
|
||||
fontWeight: FontWeight.w600),
|
||||
),
|
||||
),
|
||||
|
||||
// Tanggal + jam
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('dd MMM yyyy', 'id_ID').format(item.timestamp),
|
||||
style: const TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
Text(
|
||||
DateFormat('HH:mm:ss').format(item.timestamp),
|
||||
// ── Baris utama ───────────────────────────────────
|
||||
Row(
|
||||
children: [
|
||||
// Nomor
|
||||
SizedBox(
|
||||
width: 28,
|
||||
child: Text(
|
||||
'${index + 1}',
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade500,
|
||||
fontFamily: 'monospace'),
|
||||
fontSize: 12,
|
||||
color: Colors.grey.shade400,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Tanggal + jam
|
||||
Expanded(
|
||||
flex: 3,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
DateFormat('dd MMM yyyy', 'id_ID').format(item.timestamp),
|
||||
style: const TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.w600),
|
||||
),
|
||||
Text(
|
||||
DateFormat('HH:mm:ss').format(item.timestamp),
|
||||
style: TextStyle(
|
||||
fontSize: 11,
|
||||
color: Colors.grey.shade500,
|
||||
fontFamily: 'monospace',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Avg + max windweg
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'avg ${item.windwegKm.toStringAsFixed(3)} km',
|
||||
style: const TextStyle(
|
||||
fontSize: 12, fontWeight: FontWeight.bold),
|
||||
),
|
||||
if (item.maxWindwegKm > 0)
|
||||
Text(
|
||||
'max ${item.maxWindwegKm.toStringAsFixed(3)} km',
|
||||
style: TextStyle(
|
||||
fontSize: 10, color: Colors.orange.shade600),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Badge status
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _alertColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: _alertColor.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(_alertIcon, size: 11, color: _alertColor),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
_alertLevel,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: _alertColor,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Kecepatan
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${item.speed.toStringAsFixed(3)} m/s',
|
||||
style: const TextStyle(
|
||||
fontSize: 13, fontWeight: FontWeight.bold),
|
||||
),
|
||||
Text(
|
||||
'${kmh.toStringAsFixed(2)} km/h',
|
||||
style: TextStyle(fontSize: 11, color: Colors.grey.shade400),
|
||||
),
|
||||
],
|
||||
// ── Baris kedua: total + pulsa + sampel ───────────
|
||||
if (hasTotal) ...[
|
||||
const SizedBox(height: 6),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 28),
|
||||
child: Wrap(
|
||||
spacing: 6,
|
||||
runSpacing: 4,
|
||||
children: [
|
||||
_StatChip(
|
||||
icon: Icons.route_rounded,
|
||||
label: '${item.totalWindwegKm.toStringAsFixed(3)} km total',
|
||||
tooltip: 'Total jarak angin periode ini',
|
||||
color: Colors.blue.shade700,
|
||||
),
|
||||
_StatChip(
|
||||
icon: Icons.bolt_rounded,
|
||||
label: '${item.totalPulse} pulsa',
|
||||
tooltip: 'Total pulsa terdeteksi',
|
||||
color: Colors.purple.shade600,
|
||||
),
|
||||
if (item.sampleCount > 0)
|
||||
_StatChip(
|
||||
icon: Icons.analytics_outlined,
|
||||
label: '${item.sampleCount} sampel',
|
||||
tooltip: 'Jumlah interval average dalam periode ini',
|
||||
color: Colors.teal.shade600,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 10),
|
||||
|
||||
// Badge status
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: _alertColor.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: _alertColor.withValues(alpha: 0.4)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(_alertIcon, size: 11, color: _alertColor),
|
||||
const SizedBox(width: 3),
|
||||
Text(
|
||||
_alertLevel,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: _alertColor,
|
||||
fontWeight: FontWeight.w700),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ── Chip kecil untuk stat di history tile ──────────────────────
|
||||
class _StatChip extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String label;
|
||||
final String tooltip;
|
||||
final Color color;
|
||||
|
||||
const _StatChip({
|
||||
required this.icon,
|
||||
required this.label,
|
||||
required this.tooltip,
|
||||
required this.color,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Tooltip(
|
||||
message: tooltip,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.08),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: color.withValues(alpha: 0.25)),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: 11, color: color),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
color: color,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// ════════════════════════════════════════════════════════════
|
||||
// Delete Bottom Sheet
|
||||
// ════════════════════════════════════════════════════════════
|
||||
|
|
|
|||
|
|
@ -1,12 +1,16 @@
|
|||
class MyWindSpeed {
|
||||
final double windwegKm;
|
||||
final double totalWindwegKm;
|
||||
final double maxWindwegKm;
|
||||
final int totalPulse;
|
||||
final int sampleCount;
|
||||
final DateTime timestamp;
|
||||
|
||||
MyWindSpeed({
|
||||
required this.windwegKm,
|
||||
this.totalWindwegKm = 0.0,
|
||||
this.maxWindwegKm = 0.0,
|
||||
this.totalPulse = 0,
|
||||
this.sampleCount = 0,
|
||||
required this.timestamp,
|
||||
});
|
||||
|
|
@ -22,9 +26,6 @@ class MyWindSpeed {
|
|||
|
||||
factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) {
|
||||
return MyWindSpeed(
|
||||
// realtime → windweg_km
|
||||
// history → avg_windweg_km
|
||||
// average → windweg_km
|
||||
windwegKm:
|
||||
((json['windweg_km'] ??
|
||||
json['avg_windweg_km'] ??
|
||||
|
|
@ -32,7 +33,9 @@ class MyWindSpeed {
|
|||
json['speed'] ??
|
||||
0))
|
||||
.toDouble(),
|
||||
totalWindwegKm: (json['total_windweg_km'] ?? 0).toDouble(),
|
||||
maxWindwegKm: (json['max_windweg_km'] ?? 0).toDouble(),
|
||||
totalPulse: (json['total_pulse'] ?? 0) as int,
|
||||
sampleCount: (json['sample_count'] ?? 0) as int,
|
||||
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||
(json['timestamp'] ?? 0) * 1000,
|
||||
|
|
|
|||
Loading…
Reference in New Issue