diff --git a/lib/core/utils/time_series_mapper.dart b/lib/core/utils/time_series_mapper.dart index fd2df54..5cc4ac0 100644 --- a/lib/core/utils/time_series_mapper.dart +++ b/lib/core/utils/time_series_mapper.dart @@ -8,17 +8,24 @@ class TimeSeriesMapper { required double Function(T) getValue, }) { final now = DateTime.now(); - final slots = List.filled(24, 0.0); + + final sums = List.filled(24, 0.0); + final counts = List.filled(24, 0); for (final item in data) { final time = getTime(item); if (_isSameDay(time, now)) { - slots[time.hour] = getValue(item); + final hour = time.hour; + sums[hour] += getValue(item); + counts[hour]++; } } - return slots; + return List.generate(24, (i) { + if (counts[i] == 0) return 0; + return sums[i] / counts[i]; // 🔥 average, bukan overwrite + }); } /// ========================= @@ -32,10 +39,9 @@ class TimeSeriesMapper { final now = DateTime.now(); final slots = List.filled(7, 0.0); - DateTime startOfWeek = - now.subtract(Duration(days: now.weekday - 1)); - startOfWeek = DateTime( - startOfWeek.year, startOfWeek.month, startOfWeek.day); + DateTime startOfWeek = now.subtract(Duration(days: now.weekday - 1)); + startOfWeek = + DateTime(startOfWeek.year, startOfWeek.month, startOfWeek.day); for (final item in data) { final time = getTime(item); @@ -75,8 +81,23 @@ class TimeSeriesMapper { /// 🧠 HELPER /// ========================= static bool _isSameDay(DateTime a, DateTime b) { - return a.year == b.year && - a.month == b.month && - a.day == b.day; + return a.year == b.year && a.month == b.month && a.day == b.day; } -} \ No newline at end of file + + static List smooth(List data) { + if (data.length < 3) return data; + + List result = []; + + for (int i = 0; i < data.length; i++) { + if (i == 0 || i == data.length - 1) { + result.add(data[i]); + } else { + final avg = (data[i - 1] + data[i] + data[i + 1]) / 3; + result.add(avg); + } + } + + return result; + } +} diff --git a/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart b/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart index 5cdc2c8..ef52fb0 100644 --- a/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart +++ b/lib/screens/auth/blocs/sign_in_bloc/sign_in_bloc.dart @@ -22,6 +22,7 @@ class SignInBloc extends Bloc { emit(SignInProcess()); try { await _userRepository.signInWithGoogle(); + emit(SignInSuccess()); } catch (e) { emit(SignInFailure()); emit(SignInInitial()); diff --git a/lib/screens/monitoring/air_quality_monitoring_screen.dart b/lib/screens/monitoring/air_quality_monitoring_screen.dart deleted file mode 100644 index ea75308..0000000 --- a/lib/screens/monitoring/air_quality_monitoring_screen.dart +++ /dev/null @@ -1,784 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:fl_chart/fl_chart.dart'; -import 'shared/widgets/monitoring_shared.dart'; - -/// AIR QUALITY MONITORING SCREEN -class AirQualityMonitoringScreen extends StatefulWidget { - const AirQualityMonitoringScreen({super.key}); - - @override - State createState() => - _AirQualityMonitoringScreenState(); -} - -class _AirQualityMonitoringScreenState - extends State { - String _selectedPeriod = "Hari Ini"; - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text("Monitoring Kualitas Udara"), - centerTitle: true, - ), - body: Padding( - padding: const EdgeInsets.all(16), - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Kualitas Udara", - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 20), - - /// GRAFIK MONITORING - _buildMonitoringGraphBlock(), - const SizedBox(height: 25), - - /// PENJELASAN - _buildAirQualityExplanation(), - const SizedBox(height: 25), - - /// STATISTIK TAHUNAN - _buildAnnualAirQualityGraphSection(), - ], - ), - ), - ), - ); - } - - /// EXPORT FUNCTIONS - void _exportDailyAirQualityData() { - final StringBuffer csvContent = StringBuffer(); - csvContent.writeln("DATA MONITORING KUALITAS UDARA - HARIAN (24 JAM)"); - csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}"); - csvContent.writeln(""); - csvContent.writeln("Jam,AQI (Air Quality Index)"); - - final dailyData = [ - 68.0, - 70.0, - 72.0, - 75.0, - 78.0, - 80.0, - 78.0, - 75.0, - 72.0, - 70.0, - 68.0, - 65.0, - 60.0, - 58.0, - 60.0, - 62.0, - 65.0, - 68.0, - 70.0, - 72.0, - 75.0, - 78.0, - 76.0, - 74.0 - ]; - for (int i = 0; i < dailyData.length; i++) { - csvContent.writeln("$i:00,${dailyData[i]}"); - } - - csvContent.writeln(""); - csvContent.writeln("Statistik Harian"); - csvContent.writeln("Total: 1709 AQI"); - csvContent.writeln("Rata-rata: 71.2 AQI"); - csvContent.writeln("Maksimal: 80 AQI"); - csvContent.writeln("Minimal: 58 AQI"); - - showExportPreview( - context, csvContent.toString(), "Data Harian Kualitas Udara"); - } - - void _exportWeeklyAirQualityData() { - final StringBuffer csvContent = StringBuffer(); - csvContent.writeln("DATA MONITORING KUALITAS UDARA - MINGGUAN"); - csvContent.writeln("Tanggal: ${DateTime.now().toString().split(' ')[0]}"); - csvContent.writeln(""); - csvContent.writeln("Hari,Rata-rata AQI"); - - final days = [ - 'Senin', - 'Selasa', - 'Rabu', - 'Kamis', - 'Jumat', - 'Sabtu', - 'Minggu' - ]; - final weeklyData = [68.0, 72.0, 75.0, 78.0, 72.0, 68.0, 62.0]; - - for (int i = 0; i < days.length; i++) { - csvContent.writeln("${days[i]},${weeklyData[i]}"); - } - - csvContent.writeln(""); - csvContent.writeln("Statistik Mingguan"); - csvContent.writeln("Total: 495 AQI"); - csvContent.writeln("Rata-rata: 70.7 AQI"); - csvContent.writeln("Maksimal: 78 AQI"); - csvContent.writeln("Minimal: 62 AQI"); - - showExportPreview( - context, csvContent.toString(), "Data Mingguan Kualitas Udara"); - } - - void _exportMonthlyAirQualityData() { - final StringBuffer csvContent = StringBuffer(); - csvContent.writeln("DATA MONITORING KUALITAS UDARA - BULANAN"); - csvContent.writeln( - "Bulan: ${DateTime.now().toString().split(' ')[0].substring(0, 7)}"); - csvContent.writeln(""); - csvContent.writeln("Tanggal,Rata-rata AQI"); - - for (int i = 1; i <= 28; i++) { - final value = (70.0 + (i % 6) * 2.5).toStringAsFixed(1); - csvContent.writeln("$i,$value"); - } - - csvContent.writeln(""); - csvContent.writeln("Statistik Bulanan"); - csvContent.writeln("Total: 1990 AQI"); - csvContent.writeln("Rata-rata: 71.1 AQI"); - csvContent.writeln("Maksimal: 82 AQI"); - csvContent.writeln("Minimal: 55 AQI"); - - showExportPreview( - context, csvContent.toString(), "Data Bulanan Kualitas Udara"); - } - - void _exportAnnualAirQualityData() { - final StringBuffer csvContent = StringBuffer(); - csvContent.writeln("DATA MONITORING KUALITAS UDARA - TAHUNAN"); - csvContent.writeln("Tahun: 2025"); - csvContent.writeln(""); - csvContent.writeln("Bulan,Rata-rata AQI,AQI Max"); - - final months = [ - 'Januari', - 'Februari', - 'Maret', - 'April', - 'Mei', - 'Juni', - 'Juli', - 'Agustus', - 'September', - 'Oktober', - 'November', - 'Desember' - ]; - final averages = [ - 68.0, - 70.0, - 72.5, - 75.0, - 78.0, - 80.0, - 82.0, - 80.0, - 75.0, - 70.0, - 68.0, - 65.0 - ]; - final maxAqi = [ - 85.0, - 88.0, - 90.0, - 95.0, - 98.0, - 100.0, - 102.0, - 100.0, - 95.0, - 90.0, - 85.0, - 80.0 - ]; - - for (int i = 0; i < months.length; i++) { - csvContent.writeln("${months[i]},${averages[i]},${maxAqi[i]}"); - } - - csvContent.writeln(""); - csvContent.writeln("STATISTIK TAHUNAN"); - csvContent.writeln("Rata-rata Tahunan: 74.2 AQI"); - csvContent.writeln("AQI Max Teringgi: 102.0"); - csvContent.writeln("AQI Min Terendah: 65.0"); - - showExportPreview( - context, csvContent.toString(), "Data Tahunan Kualitas Udara"); - } - - /// DATA GENERATORS - List _getAirQualityPeriodData() { - switch (_selectedPeriod) { - case "Hari Ini": - return List.generate(24, (i) { - final values = [ - 68.0, - 70.0, - 72.0, - 75.0, - 78.0, - 80.0, - 78.0, - 75.0, - 72.0, - 70.0, - 68.0, - 65.0, - 60.0, - 58.0, - 60.0, - 62.0, - 65.0, - 68.0, - 70.0, - 72.0, - 75.0, - 78.0, - 76.0, - 74.0 - ]; - return FlSpot(i.toDouble(), values[i]); - }); - case "Minggu Ini": - return [ - const FlSpot(0, 68.0), - const FlSpot(1, 72.0), - const FlSpot(2, 75.0), - const FlSpot(3, 78.0), - const FlSpot(4, 72.0), - const FlSpot(5, 68.0), - const FlSpot(6, 62.0), - ]; - case "Bulan Ini": - return [ - const FlSpot(0, 70.0), - const FlSpot(1, 72.5), - const FlSpot(2, 75.0), - const FlSpot(3, 77.5), - const FlSpot(4, 80.0), - const FlSpot(5, 82.0), - const FlSpot(6, 80.0), - const FlSpot(7, 78.0), - const FlSpot(8, 75.0), - const FlSpot(9, 72.0), - const FlSpot(10, 70.0), - const FlSpot(11, 68.0), - ]; - default: - return []; - } - } - - /// UI BUILDERS - Widget _buildMonitoringGraphBlock() { - return Card( - elevation: 4, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - "Status Kualitas Udara", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 5, - ), - decoration: BoxDecoration( - color: Colors.amber.shade100, - borderRadius: BorderRadius.circular(20), - ), - child: const Text( - "Normal", - style: TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: Colors.amber, - ), - ), - ), - ], - ), - const SizedBox(height: 16), - - /// PERIOD SELECTOR + EXPORT - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: SingleChildScrollView( - scrollDirection: Axis.horizontal, - child: Row( - children: - ["Hari Ini", "Minggu Ini", "Bulan Ini"].map((period) { - return Padding( - padding: const EdgeInsets.only(right: 8), - child: ChoiceChip( - label: Text(period), - selected: _selectedPeriod == period, - onSelected: (selected) { - setState(() { - _selectedPeriod = period; - }); - }, - selectedColor: Colors.orange, - labelStyle: TextStyle( - color: _selectedPeriod == period - ? Colors.white - : Colors.black, - ), - ), - ); - }).toList(), - ), - ), - ), - const SizedBox(width: 8), - ElevatedButton.icon( - onPressed: () { - if (_selectedPeriod == "Hari Ini") { - _exportDailyAirQualityData(); - } else if (_selectedPeriod == "Minggu Ini") { - _exportWeeklyAirQualityData(); - } else if (_selectedPeriod == "Bulan Ini") { - _exportMonthlyAirQualityData(); - } - }, - icon: const Icon(Icons.download, size: 16), - label: const Text("Export"), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.orange, - foregroundColor: Colors.white, - padding: const EdgeInsets.symmetric( - horizontal: 10, - vertical: 6, - ), - textStyle: const TextStyle(fontSize: 11), - ), - ), - ], - ), - const SizedBox(height: 16), - - /// GRAPH - SizedBox( - height: 300, - child: LineChart( - LineChartData( - gridData: FlGridData( - show: true, - drawHorizontalLine: true, - drawVerticalLine: false, - horizontalInterval: 1, - getDrawingHorizontalLine: (value) { - return FlLine( - color: Colors.grey.shade300, - strokeWidth: 1, - ); - }, - ), - titlesData: FlTitlesData( - show: true, - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - interval: getPeriodInterval(_selectedPeriod), - getTitlesWidget: (value, meta) { - return Text( - getPeriodLabel(_selectedPeriod, value), - style: const TextStyle( - fontSize: 10, - color: Colors.grey, - ), - ); - }, - ), - ), - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - interval: 1, - getTitlesWidget: (value, meta) { - return Text( - value.toInt().toString(), - style: const TextStyle( - fontSize: 9, - color: Colors.grey, - ), - ); - }, - reservedSize: 28, - ), - ), - topTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - rightTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - ), - borderData: FlBorderData( - show: true, - border: Border( - left: BorderSide( - color: Colors.grey.shade300, - width: 1, - ), - bottom: BorderSide( - color: Colors.grey.shade300, - width: 1, - ), - ), - ), - lineBarsData: [ - LineChartBarData( - spots: _getAirQualityPeriodData(), - isCurved: true, - barWidth: 2.5, - dotData: FlDotData( - show: true, - getDotPainter: (spot, percent, barData, index) { - return FlDotCirclePainter( - radius: 4, - color: Colors.orange, - strokeWidth: 0, - ); - }, - ), - color: Colors.orange, - belowBarData: BarAreaData( - show: true, - color: Colors.orange.withOpacity(0.1), - ), - ), - ], - ), - ), - ), - ], - ), - ), - ); - } - - Widget _buildAirQualityExplanation() { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.orange.shade50, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.orange.shade200), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Penjelasan Kualitas Udara", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.bold, - color: Colors.orange, - ), - ), - const SizedBox(height: 10), - const Text( - "Kualitas udara diukur menggunakan Air Quality Index (AQI) yang menunjukkan tingkat polusi udara. AQI berkisar dari 0-500, dengan nilai lebih rendah menunjukkan udara lebih bersih. Monitoring kualitas udara penting untuk kesehatan masyarakat, perencanaan lingkungan, dan identifikasi sumber polusi. Kategori: 0-50 (Baik), 51-100 (Wajar), 101-150 (Tidak Sehat), 151+ (Sangat Tidak Sehat).", - style: TextStyle( - fontSize: 12, - height: 1.6, - color: Colors.orange, - ), - ), - ], - ), - ); - } - - Widget _buildAnnualAirQualityGraphSection() { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "Statistik Tahunan Kualitas Udara", - style: TextStyle( - fontSize: 16, - fontWeight: FontWeight.bold, - ), - ), - const SizedBox(height: 15), - - /// STATS - Row( - children: [ - Expanded( - child: StatCard( - title: "Rata-rata Tahunan", - value: "74.2 AQI", - color: Colors.orange, - icon: Icons.cloud, - ), - ), - const SizedBox(width: 10), - Expanded( - child: StatCard( - title: "AQI Maksimal", - value: "102.0", - color: Colors.red, - icon: Icons.warning, - ), - ), - ], - ), - const SizedBox(height: 20), - - /// CHART - const Text( - "Grafik Tahunan - Rata-rata & AQI Maksimal", - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 12), - Card( - elevation: 3, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: SizedBox( - height: 320, - child: LineChart( - LineChartData( - gridData: FlGridData( - show: true, - drawHorizontalLine: true, - drawVerticalLine: false, - horizontalInterval: 5, - getDrawingHorizontalLine: (value) { - return FlLine( - color: Colors.grey.shade300, - strokeWidth: 1, - ); - }, - ), - titlesData: FlTitlesData( - show: true, - bottomTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - getTitlesWidget: (value, meta) { - final months = [ - 'Jan', - 'Feb', - 'Mar', - 'Apr', - 'Mei', - 'Jun', - 'Jul', - 'Agu', - 'Sep', - 'Okt', - 'Nov', - 'Des' - ]; - return Text( - months[value.toInt()], - style: const TextStyle( - fontSize: 10, - color: Colors.grey, - ), - ); - }, - reservedSize: 30, - ), - ), - leftTitles: AxisTitles( - sideTitles: SideTitles( - showTitles: true, - getTitlesWidget: (value, meta) { - return Text( - '${value.toInt()} AQI', - style: const TextStyle( - fontSize: 9, - color: Colors.grey, - ), - ); - }, - reservedSize: 50, - ), - ), - rightTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - topTitles: AxisTitles( - sideTitles: SideTitles(showTitles: false), - ), - ), - borderData: FlBorderData( - show: true, - border: Border( - left: BorderSide( - color: Colors.grey.shade300, - width: 1, - ), - bottom: BorderSide( - color: Colors.grey.shade300, - width: 1, - ), - ), - ), - lineBarsData: [ - /// Rata-rata - LineChartBarData( - spots: const [ - FlSpot(0, 68.0), - FlSpot(1, 70.0), - FlSpot(2, 72.5), - FlSpot(3, 75.0), - FlSpot(4, 78.0), - FlSpot(5, 80.0), - FlSpot(6, 82.0), - FlSpot(7, 80.0), - FlSpot(8, 75.0), - FlSpot(9, 70.0), - FlSpot(10, 68.0), - FlSpot(11, 65.0), - ], - isCurved: true, - barWidth: 2.5, - dotData: FlDotData( - show: true, - getDotPainter: (spot, percent, barData, index) { - return FlDotCirclePainter( - radius: 4, - color: Colors.orange, - strokeWidth: 0, - ); - }, - ), - color: Colors.orange, - belowBarData: BarAreaData( - show: true, - color: Colors.orange.withOpacity(0.1), - ), - ), - - /// AQI Max - LineChartBarData( - spots: const [ - FlSpot(0, 85.0), - FlSpot(1, 88.0), - FlSpot(2, 90.0), - FlSpot(3, 95.0), - FlSpot(4, 98.0), - FlSpot(5, 100.0), - FlSpot(6, 102.0), - FlSpot(7, 100.0), - FlSpot(8, 95.0), - FlSpot(9, 90.0), - FlSpot(10, 85.0), - FlSpot(11, 80.0), - ], - isCurved: true, - barWidth: 2.5, - dotData: FlDotData( - show: true, - getDotPainter: (spot, percent, barData, index) { - return FlDotCirclePainter( - radius: 4, - color: Colors.red, - strokeWidth: 0, - ); - }, - ), - color: Colors.red, - ), - ], - ), - ), - ), - ), - ), - const SizedBox(height: 15), - - /// LEGEND - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Container( - width: 16, - height: 16, - decoration: BoxDecoration( - color: Colors.orange, - borderRadius: BorderRadius.circular(3), - ), - ), - const SizedBox(width: 8), - const Text( - "Rata-rata AQI", - style: TextStyle(fontSize: 12), - ), - const SizedBox(width: 20), - Container( - width: 16, - height: 16, - decoration: BoxDecoration( - color: Colors.red, - borderRadius: BorderRadius.circular(3), - ), - ), - const SizedBox(width: 8), - const Text( - "AQI Maksimal", - style: TextStyle(fontSize: 12), - ), - ], - ), - const SizedBox(height: 15), - Center( - child: ElevatedButton.icon( - onPressed: () => _exportAnnualAirQualityData(), - icon: const Icon(Icons.file_download), - label: const Text("Download Excel Tahunan"), - style: ElevatedButton.styleFrom( - backgroundColor: Colors.green, - foregroundColor: Colors.white, - ), - ), - ), - ], - ); - } -} diff --git a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart index f7b18ef..7544b4e 100644 --- a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart +++ b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart @@ -15,7 +15,6 @@ class WindSpeedBloc extends Bloc { WindSpeedBloc({required MonitoringRepository repository}) : _repository = repository, super(const WindSpeedState()) { - /// 🔥 START MONITORING on(_onStarted, transformer: restartable()); @@ -30,24 +29,24 @@ class WindSpeedBloc extends Bloc { /// 🚀 START /// ========================= Future _onStarted( - WatchWindSpeedStarted event, - Emitter emit, - ) async { - + WatchWindSpeedStarted event, + Emitter emit, + ) async { emit(state.copyWith(isLoading: true)); /// 1. Ambil history SEKALI final history = await _repository.getSensorHistory( 'anemometer/history', - (json) => MyWindSpeed.fromJson(json), + (json) => MyWindSpeed.fromJson(json), ); /// 2. Mapping default (harian) - final dailyGraph = TimeSeriesMapper.toDaily( + final raw = TimeSeriesMapper.toDaily( data: history, - getTime: (e) => e.timestamp, - getValue: (e) => e.speed, - ); + getTime: (e) => e.timestamp, + getValue: (e) => e.speed, + ); + final dailyGraph = TimeSeriesMapper.smooth(raw); emit(state.copyWith( history: history, @@ -61,7 +60,7 @@ class WindSpeedBloc extends Bloc { _subscription = _repository .getSensorStream( 'anemometer/realtime', - (json) => MyWindSpeed.fromJson(json), + (json) => MyWindSpeed.fromJson(json), ) .listen((data) { add(_WindSpeedRealtimeUpdated(data)); @@ -72,22 +71,32 @@ class WindSpeedBloc extends Bloc { /// ⚡ REALTIME UPDATE /// ========================= void _onRealtimeUpdated( - _WindSpeedRealtimeUpdated event, - Emitter emit, - ) { + _WindSpeedRealtimeUpdated event, + Emitter emit, + ) { + final updated = List.from(state.dailySpeeds); - final updatedSpeeds = List.from(state.dailySpeeds); + final index = DateTime.now().hour; + double newValue = event.data.speed; - /// 🔥 FIX: pakai JAM bukan menit - final int index = DateTime.now().hour; + if (index < updated.length) { + final lastValue = updated[index]; - if (index < updatedSpeeds.length) { - updatedSpeeds[index] = event.data.speed; + /// 🔥 ANTI SPIKE + SMOOTHING + if (lastValue != 0) { + if ((newValue - lastValue).abs() > 15) { + newValue = lastValue; // buang spike + } else { + newValue = (lastValue + newValue) / 2; // smoothing + } + } + + updated[index] = newValue; } emit(state.copyWith( - currentSpeed: event.data.speed, - dailySpeeds: updatedSpeeds, + currentSpeed: newValue, + dailySpeeds: updated, )); } @@ -95,10 +104,9 @@ class WindSpeedBloc extends Bloc { /// 📊 CHANGE PERIOD /// ========================= Future _onPeriodChanged( - WindSpeedPeriodChanged event, - Emitter emit, - ) async { - + WindSpeedPeriodChanged event, + Emitter emit, + ) async { emit(state.copyWith(isLoading: true, selectedPeriod: event.period)); final history = state.history; @@ -106,24 +114,24 @@ class WindSpeedBloc extends Bloc { List updatedGraph; if (event.period == "Minggu Ini") { - updatedGraph = TimeSeriesMapper.toWeekly( - data: history, - getTime: (e) => e.timestamp, - getValue: (e) => e.speed, - ); -} else if (event.period == "Bulan Ini") { - updatedGraph = TimeSeriesMapper.toMonthly( - data: history, - getTime: (e) => e.timestamp, - getValue: (e) => e.speed, - ); -} else { - updatedGraph = TimeSeriesMapper.toDaily( - data: history, - getTime: (e) => e.timestamp, - getValue: (e) => e.speed, - ); -} + updatedGraph = TimeSeriesMapper.toWeekly( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.speed, + ); + } else if (event.period == "Bulan Ini") { + updatedGraph = TimeSeriesMapper.toMonthly( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.speed, + ); + } else { + updatedGraph = TimeSeriesMapper.toDaily( + data: history, + getTime: (e) => e.timestamp, + getValue: (e) => e.speed, + ); + } emit(state.copyWith( dailySpeeds: updatedGraph, @@ -148,4 +156,4 @@ class _WindSpeedRealtimeUpdated extends WindSpeedEvent { @override List get props => [data]; -} \ No newline at end of file +} diff --git a/packages/user_repository/lib/src/firebase_user_repo.dart b/packages/user_repository/lib/src/firebase_user_repo.dart index 14e67a3..f816dc5 100644 --- a/packages/user_repository/lib/src/firebase_user_repo.dart +++ b/packages/user_repository/lib/src/firebase_user_repo.dart @@ -4,6 +4,7 @@ import 'package:firebase_auth/firebase_auth.dart'; import 'package:rxdart/rxdart.dart'; import 'package:user_repository/user_repository.dart'; import 'package:google_sign_in/google_sign_in.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; class FirebaseUserRepo implements UserRepository { final FirebaseAuth _firebaseAuth; @@ -96,38 +97,25 @@ class FirebaseUserRepo implements UserRepository { @override Future signInWithGoogle() async { try { - final GoogleSignIn googleSignIn = GoogleSignIn(); + if (kIsWeb) { + final googleProvider = GoogleAuthProvider(); + await _firebaseAuth.signInWithPopup(googleProvider); + } else { + final GoogleSignIn googleSignIn = GoogleSignIn(); + final googleUser = await googleSignIn.signIn(); - final GoogleSignInAccount? googleUser = await googleSignIn.signIn(); - - if (googleUser == null) return; - - final GoogleSignInAuthentication googleAuth = - await googleUser.authentication; - - final credential = GoogleAuthProvider.credential( - accessToken: googleAuth.accessToken, - idToken: googleAuth.idToken, - ); - - final userCredential = - await _firebaseAuth.signInWithCredential(credential); - - final user = userCredential.user; - - if (user != null) { - final existingUser = await userCollection.doc(user.uid).get(); - - if (!existingUser.exists) { - final newUser = MyUser( - userId: user.uid, - email: user.email ?? '', - name: user.displayName ?? 'User', - hasActiveCart: false, - ); - - await setUserData(newUser); + if (googleUser == null) { + throw Exception("cancelled"); } + + final GoogleSignInAuthentication googleAuth = + await googleUser.authentication; + + final credential = GoogleAuthProvider.credential( + accessToken: googleAuth.accessToken, + idToken: googleAuth.idToken, + ); + await _firebaseAuth.signInWithCredential(credential); } } catch (e) { log('Google sign-in error: $e');