diff --git a/android/build/reports/problems/problems-report.html b/android/build/reports/problems/problems-report.html
new file mode 100644
index 0000000..6bb5e2c
--- /dev/null
+++ b/android/build/reports/problems/problems-report.html
@@ -0,0 +1,663 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ Gradle Configuration Cache
+
+
+
+
+
+
+ Loading...
+
+
+
+
+
+
+
diff --git a/lib/app_view.dart b/lib/app_view.dart
index 0320ddb..f8b4763 100644
--- a/lib/app_view.dart
+++ b/lib/app_view.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:klimatologiot/blocs/authentication_bloc/authentication_bloc.dart';
-
+import 'package:flutter_localizations/flutter_localizations.dart';
import 'package:klimatologiot/screens/auth/views/welcome_screen.dart';
import 'package:klimatologiot/screens/home/views/home_screen.dart';
@@ -13,6 +13,15 @@ class MyAppView extends StatelessWidget {
return MaterialApp(
title: 'Klimatologi',
debugShowCheckedModeBanner: false,
+ localizationsDelegates: const [
+ GlobalMaterialLocalizations.delegate,
+ GlobalWidgetsLocalizations.delegate,
+ GlobalCupertinoLocalizations.delegate,
+ ],
+ supportedLocales: const [
+ Locale('id', 'ID'),
+ Locale('en'),
+ ],
theme: ThemeData(
colorScheme: ColorScheme.light(
surface: Colors.grey.shade100,
diff --git a/lib/screens/monitoring/shared/utils/excel/excel_saver_mobile.dart b/lib/screens/monitoring/shared/utils/excel/excel_saver_mobile.dart
new file mode 100644
index 0000000..e0e9b16
--- /dev/null
+++ b/lib/screens/monitoring/shared/utils/excel/excel_saver_mobile.dart
@@ -0,0 +1,19 @@
+import 'dart:io';
+import 'dart:typed_data';
+import 'package:path_provider/path_provider.dart';
+import 'package:share_plus/share_plus.dart';
+
+/// Mobile (Android/iOS): simpan ke temp dir lalu buka share sheet
+Future saveAndShareExcel(Uint8List bytes, String fileName) async {
+ final dir = await getTemporaryDirectory();
+ final file = File('${dir.path}/$fileName');
+ await file.writeAsBytes(bytes, flush: true);
+
+ await Share.shareXFiles(
+ [XFile(
+ file.path,
+ mimeType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ )],
+ subject: fileName,
+ );
+}
diff --git a/lib/screens/monitoring/shared/utils/excel/excel_saver_stub.dart b/lib/screens/monitoring/shared/utils/excel/excel_saver_stub.dart
new file mode 100644
index 0000000..8c86ee5
--- /dev/null
+++ b/lib/screens/monitoring/shared/utils/excel/excel_saver_stub.dart
@@ -0,0 +1,6 @@
+import 'dart:typed_data';
+
+// Stub — tidak pernah dipanggil langsung, digantikan web atau mobile
+Future saveAndShareExcel(Uint8List bytes, String fileName) async {
+ throw UnsupportedError('Platform tidak didukung');
+}
diff --git a/lib/screens/monitoring/shared/utils/excel/excel_saver_web.dart b/lib/screens/monitoring/shared/utils/excel/excel_saver_web.dart
new file mode 100644
index 0000000..eb0bc20
--- /dev/null
+++ b/lib/screens/monitoring/shared/utils/excel/excel_saver_web.dart
@@ -0,0 +1,16 @@
+// ignore: avoid_web_libraries_in_flutter
+import 'dart:html' as html;
+import 'dart:typed_data';
+
+/// Web: langsung trigger download lewat browser
+Future saveAndShareExcel(Uint8List bytes, String fileName) async {
+ final blob = html.Blob(
+ [bytes],
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
+ );
+ final url = html.Url.createObjectUrlFromBlob(blob);
+ final anchor = html.AnchorElement(href: url)
+ ..setAttribute('download', fileName)
+ ..click();
+ html.Url.revokeObjectUrl(url);
+}
diff --git a/lib/screens/monitoring/shared/utils/excel/wind_speed_excel_service.dart b/lib/screens/monitoring/shared/utils/excel/wind_speed_excel_service.dart
index 1ec30f0..445f846 100644
--- a/lib/screens/monitoring/shared/utils/excel/wind_speed_excel_service.dart
+++ b/lib/screens/monitoring/shared/utils/excel/wind_speed_excel_service.dart
@@ -8,10 +8,12 @@
// share_plus: ^10.0.2
// ===========================================================
-import 'dart:io';
+import 'dart:typed_data';
import 'package:excel/excel.dart';
+import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:intl/intl.dart';
import 'package:monitoring_repository/monitoring_repository.dart';
+import 'dart:io';
import 'package:path_provider/path_provider.dart';
import 'package:share_plus/share_plus.dart';
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 9a995a7..e618a2c 100644
--- a/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart
+++ b/lib/screens/monitoring/wind_speed/blocs/wind_speed_bloc.dart
@@ -19,38 +19,32 @@ class WindSpeedBloc extends Bloc {
final NotificationBloc _notificationBloc;
StreamSubscription? _subscription;
- WindSpeedBloc(
- {required MonitoringRepository repository,
- required NotificationBloc notificationBloc})
- : _repository = repository,
+ WindSpeedBloc({
+ required MonitoringRepository repository,
+ required NotificationBloc notificationBloc,
+ }) : _repository = repository,
_notificationBloc = notificationBloc,
super(const WindSpeedState()) {
- /// 🔥 START MONITORING
on(_onStarted, transformer: restartable());
-
- /// 🔥 REALTIME UPDATE
on<_WindSpeedRealtimeUpdated>(_onRealtimeUpdated);
-
- /// 🔥 CHANGE PERIOD
on(_onPeriodChanged);
+ on(_onDateFilterChanged); // ← baru
}
- /// =========================
- /// 🚀 START
- /// =========================
+ // ════════════════════════════════════════════════════════════
+ // START
+ // ════════════════════════════════════════════════════════════
Future _onStarted(
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),
);
- // ✅ Ini saja yang benar
final dailyGraph = TimeSeriesMapper.smooth(
TimeSeriesMapper.toDaily(
data: history,
@@ -58,7 +52,6 @@ class WindSpeedBloc extends Bloc {
getValue: (e) => e.speed,
),
);
-
final weekly = TimeSeriesMapper.smooth(
TimeSeriesMapper.toWeekly(
data: history,
@@ -66,7 +59,6 @@ class WindSpeedBloc extends Bloc {
getValue: (e) => e.speed,
),
);
-
final monthly = TimeSeriesMapper.smooth(
TimeSeriesMapper.toMonthly(
data: history,
@@ -75,25 +67,22 @@ class WindSpeedBloc extends Bloc {
),
);
- // Cek kondisi awal dari history
if (history.isNotEmpty) {
_emitWindAlert(history.last.speed);
}
emit(state.copyWith(
history: history,
+ filteredHistory: history, // awal = semua data
dailySpeeds: dailyGraph,
weeklySpeeds: weekly,
monthlySpeeds: monthly,
isLoading: false,
- alertLevel: history.isNotEmpty // ← tambah ini
- ? _getAlertLevel(history.last.speed)
- : "Normal",
+ alertLevel:
+ history.isNotEmpty ? _getAlertLevel(history.last.speed) : 'Normal',
));
- /// 3. Start realtime stream (manual subscription)
await _subscription?.cancel();
-
_subscription = _repository
.getSensorStream(
'anemometer/realtime',
@@ -104,9 +93,9 @@ class WindSpeedBloc extends Bloc {
});
}
- /// =========================
- /// ⚡ REALTIME UPDATE
- /// =========================
+ // ════════════════════════════════════════════════════════════
+ // REALTIME UPDATE
+ // ════════════════════════════════════════════════════════════
void _onRealtimeUpdated(
_WindSpeedRealtimeUpdated event,
Emitter emit,
@@ -117,13 +106,11 @@ class WindSpeedBloc extends Bloc {
if (index < updated.length) {
final lastValue = updated[index];
-
- /// 🔥 ANTI SPIKE + SMOOTHING
if (lastValue != 0) {
if ((newValue - lastValue).abs() > 15) {
- newValue = lastValue; // buang spike
+ newValue = lastValue;
} else {
- newValue = (lastValue + newValue) / 2; // smoothing
+ newValue = (lastValue + newValue) / 2;
}
}
updated[index] = newValue.clamp(0, 100);
@@ -138,26 +125,24 @@ class WindSpeedBloc extends Bloc {
));
}
- /// =========================
- /// 📊 CHANGE PERIOD
- /// =========================
+ // ════════════════════════════════════════════════════════════
+ // PERIOD CHANGED
+ // ════════════════════════════════════════════════════════════
Future _onPeriodChanged(
WindSpeedPeriodChanged event,
Emitter emit,
) async {
emit(state.copyWith(selectedPeriod: event.period));
-
final history = state.history;
List raw;
-
- if (event.period == "Minggu Ini") {
+ if (event.period == 'Minggu Ini') {
raw = TimeSeriesMapper.toWeekly(
data: history,
getTime: (e) => e.timestamp,
getValue: (e) => e.speed,
);
- } else if (event.period == "Bulan Ini") {
+ } else if (event.period == 'Bulan Ini') {
raw = TimeSeriesMapper.toMonthly(
data: history,
getTime: (e) => e.timestamp,
@@ -171,35 +156,60 @@ class WindSpeedBloc extends Bloc {
);
}
- /// 🔥 baru di-smooth
final updatedGraph = TimeSeriesMapper.smooth(raw);
emit(state.copyWith(
dailySpeeds:
- event.period == "Hari Ini" ? updatedGraph : state.dailySpeeds,
+ event.period == 'Hari Ini' ? updatedGraph : state.dailySpeeds,
weeklySpeeds:
- event.period == "Minggu Ini" ? updatedGraph : state.weeklySpeeds,
+ event.period == 'Minggu Ini' ? updatedGraph : state.weeklySpeeds,
monthlySpeeds:
- event.period == "Bulan Ini" ? updatedGraph : state.monthlySpeeds,
+ event.period == 'Bulan Ini' ? updatedGraph : state.monthlySpeeds,
isLoading: false,
));
}
- @override
- Future close() async {
- await _subscription?.cancel();
- return super.close();
+ // ════════════════════════════════════════════════════════════
+ // DATE FILTER CHANGED ← baru
+ // ════════════════════════════════════════════════════════════
+ void _onDateFilterChanged(
+ WindSpeedDateFilterChanged event,
+ Emitter emit,
+ ) {
+ final date = event.date;
+ final allHistory = state.history;
+
+ if (date == null) {
+ // Reset: tampilkan semua
+ emit(state.copyWith(
+ filteredHistory: allHistory,
+ clearSelectedDate: true,
+ ));
+ return;
+ }
+
+ // Filter data yang tanggalnya sama dengan [date]
+ final filtered = allHistory.where((item) {
+ return item.timestamp.year == date.year &&
+ item.timestamp.month == date.month &&
+ item.timestamp.day == date.day;
+ }).toList();
+
+ emit(state.copyWith(
+ filteredHistory: filtered,
+ selectedDate: date,
+ ));
}
+ // ════════════════════════════════════════════════════════════
+ // HELPERS
+ // ════════════════════════════════════════════════════════════
String _getAlertLevel(double speed) {
- if (speed >= _kWindDanger) return "Bahaya";
- if (speed >= _kWindWarning) return "Waspada";
- return "Normal";
+ if (speed >= _kWindDanger) return 'Bahaya';
+ if (speed >= _kWindWarning) return 'Waspada';
+ return 'Normal';
}
-// =========================
- // HELPER: kirim alert ke NotificationBloc
- // =========================
void _emitWindAlert(double speed) {
final AlertSeverity severity;
final String message;
@@ -211,7 +221,7 @@ class WindSpeedBloc extends Bloc {
severity = AlertSeverity.warning;
message = 'Kecepatan angin ${speed.toStringAsFixed(1)} m/s — waspada';
} else {
- severity = AlertSeverity.info; // normal → bersihkan alert
+ severity = AlertSeverity.info;
message = '';
}
@@ -225,16 +235,19 @@ class WindSpeedBloc extends Bloc {
),
));
}
+
+ @override
+ Future close() async {
+ await _subscription?.cancel();
+ return super.close();
+ }
}
-/// =========================
-/// 🔒 INTERNAL EVENT (PRIVATE)
-/// =========================
+/// Internal event — tidak diekspos ke luar
class _WindSpeedRealtimeUpdated extends WindSpeedEvent {
final MyWindSpeed data;
-
const _WindSpeedRealtimeUpdated(this.data);
@override
- List