oke
This commit is contained in:
parent
74a1609c21
commit
a6e74d4da0
|
|
@ -49,7 +49,11 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
WatchWindSpeedStarted event,
|
WatchWindSpeedStarted event,
|
||||||
Emitter<WindSpeedState> emit,
|
Emitter<WindSpeedState> emit,
|
||||||
) async {
|
) 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 deviceId = await _getDeviceId();
|
||||||
|
|
||||||
final historyMap = await _repository.getSensorHistoryWithKeys(
|
final historyMap = await _repository.getSensorHistoryWithKeys(
|
||||||
|
|
@ -74,7 +78,7 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
history.isNotEmpty ? _getAlertLevel(history.last.speed) : 'Normal',
|
history.isNotEmpty ? _getAlertLevel(history.last.speed) : 'Normal',
|
||||||
));
|
));
|
||||||
|
|
||||||
await _subscription?.cancel();
|
// Subscribe stream baru
|
||||||
_subscription = _repository
|
_subscription = _repository
|
||||||
.getSensorStream(
|
.getSensorStream(
|
||||||
'anemometer/$deviceId/realtime',
|
'anemometer/$deviceId/realtime',
|
||||||
|
|
@ -90,6 +94,15 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
_WindSpeedRealtimeUpdated event,
|
_WindSpeedRealtimeUpdated event,
|
||||||
Emitter<WindSpeedState> emit,
|
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 updated = List<double>.from(state.dailySpeeds);
|
||||||
final index = DateTime.now().hour;
|
final index = DateTime.now().hour;
|
||||||
double newValue = event.data.speed;
|
double newValue = event.data.speed;
|
||||||
|
|
|
||||||
|
|
@ -747,7 +747,7 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
|
||||||
const Icon(Icons.air, color: Colors.white, size: 46),
|
const Icon(Icons.air, color: Colors.white, size: 46),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
speed.toStringAsFixed(1),
|
speed.toStringAsFixed(2),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 80,
|
fontSize: 80,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
|
|
@ -755,11 +755,11 @@ class _WindSpeedScreenState extends State<WindSpeedScreen> {
|
||||||
height: 1,
|
height: 1,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const Text('m/s',
|
const Text('km/h',
|
||||||
style: TextStyle(fontSize: 20, color: Colors.white70)),
|
style: TextStyle(fontSize: 20, color: Colors.white70)),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'${(speed * 3.6).toStringAsFixed(1)} km/h',
|
'${(speed / 3.6).toStringAsFixed(3)} m/s',
|
||||||
style: const TextStyle(fontSize: 14, color: Colors.white54),
|
style: const TextStyle(fontSize: 14, color: Colors.white54),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 14),
|
const SizedBox(height: 14),
|
||||||
|
|
@ -1056,8 +1056,8 @@ class _HistoryTile extends StatelessWidget {
|
||||||
const _HistoryTile({required this.item, required this.index});
|
const _HistoryTile({required this.item, required this.index});
|
||||||
|
|
||||||
String get _alertLevel {
|
String get _alertLevel {
|
||||||
if (item.speed >= 12.5) return 'Bahaya';
|
if (item.speed >= 45.0) return 'Bahaya';
|
||||||
if (item.speed >= 8.0) return 'Waspada';
|
if (item.speed >= 25.5) return 'Waspada';
|
||||||
return 'Normal';
|
return 'Normal';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1085,98 +1085,188 @@ class _HistoryTile extends StatelessWidget {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final kmh = item.speed * 3.6;
|
|
||||||
final bg = index.isEven ? Colors.grey.shade50 : Colors.white;
|
final bg = index.isEven ? Colors.grey.shade50 : Colors.white;
|
||||||
|
final hasTotal = item.totalWindwegKm > 0 || item.totalPulse > 0;
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
color: bg,
|
color: bg,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
child: Row(
|
// ← child pakai Column, bukan Row langsung
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Nomor
|
// ── Baris utama ───────────────────────────────────
|
||||||
SizedBox(
|
Row(
|
||||||
width: 28,
|
children: [
|
||||||
child: Text(
|
// Nomor
|
||||||
'${index + 1}',
|
SizedBox(
|
||||||
style: TextStyle(
|
width: 28,
|
||||||
fontSize: 12,
|
child: Text(
|
||||||
color: Colors.grey.shade400,
|
'${index + 1}',
|
||||||
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(
|
style: TextStyle(
|
||||||
fontSize: 11,
|
fontSize: 12,
|
||||||
color: Colors.grey.shade500,
|
color: Colors.grey.shade400,
|
||||||
fontFamily: 'monospace'),
|
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
|
// ── Baris kedua: total + pulsa + sampel ───────────
|
||||||
Expanded(
|
if (hasTotal) ...[
|
||||||
flex: 2,
|
const SizedBox(height: 6),
|
||||||
child: Column(
|
Padding(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
padding: const EdgeInsets.only(left: 28),
|
||||||
children: [
|
child: Wrap(
|
||||||
Text(
|
spacing: 6,
|
||||||
'${item.speed.toStringAsFixed(3)} m/s',
|
runSpacing: 4,
|
||||||
style: const TextStyle(
|
children: [
|
||||||
fontSize: 13, fontWeight: FontWeight.bold),
|
_StatChip(
|
||||||
),
|
icon: Icons.route_rounded,
|
||||||
Text(
|
label: '${item.totalWindwegKm.toStringAsFixed(3)} km total',
|
||||||
'${kmh.toStringAsFixed(2)} km/h',
|
tooltip: 'Total jarak angin periode ini',
|
||||||
style: TextStyle(fontSize: 11, color: Colors.grey.shade400),
|
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
|
// Delete Bottom Sheet
|
||||||
// ════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,16 @@
|
||||||
class MyWindSpeed {
|
class MyWindSpeed {
|
||||||
final double windwegKm;
|
final double windwegKm;
|
||||||
|
final double totalWindwegKm;
|
||||||
final double maxWindwegKm;
|
final double maxWindwegKm;
|
||||||
|
final int totalPulse;
|
||||||
final int sampleCount;
|
final int sampleCount;
|
||||||
final DateTime timestamp;
|
final DateTime timestamp;
|
||||||
|
|
||||||
MyWindSpeed({
|
MyWindSpeed({
|
||||||
required this.windwegKm,
|
required this.windwegKm,
|
||||||
|
this.totalWindwegKm = 0.0,
|
||||||
this.maxWindwegKm = 0.0,
|
this.maxWindwegKm = 0.0,
|
||||||
|
this.totalPulse = 0,
|
||||||
this.sampleCount = 0,
|
this.sampleCount = 0,
|
||||||
required this.timestamp,
|
required this.timestamp,
|
||||||
});
|
});
|
||||||
|
|
@ -22,9 +26,6 @@ class MyWindSpeed {
|
||||||
|
|
||||||
factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) {
|
factory MyWindSpeed.fromJson(Map<dynamic, dynamic> json) {
|
||||||
return MyWindSpeed(
|
return MyWindSpeed(
|
||||||
// realtime → windweg_km
|
|
||||||
// history → avg_windweg_km
|
|
||||||
// average → windweg_km
|
|
||||||
windwegKm:
|
windwegKm:
|
||||||
((json['windweg_km'] ??
|
((json['windweg_km'] ??
|
||||||
json['avg_windweg_km'] ??
|
json['avg_windweg_km'] ??
|
||||||
|
|
@ -32,7 +33,9 @@ class MyWindSpeed {
|
||||||
json['speed'] ??
|
json['speed'] ??
|
||||||
0))
|
0))
|
||||||
.toDouble(),
|
.toDouble(),
|
||||||
|
totalWindwegKm: (json['total_windweg_km'] ?? 0).toDouble(),
|
||||||
maxWindwegKm: (json['max_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,
|
sampleCount: (json['sample_count'] ?? 0) as int,
|
||||||
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
timestamp: DateTime.fromMillisecondsSinceEpoch(
|
||||||
(json['timestamp'] ?? 0) * 1000,
|
(json['timestamp'] ?? 0) * 1000,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue