Merge branch 'branch' of https://github.com/kleponijo/klimatologi into branch
This commit is contained in:
commit
bc9ca486c3
File diff suppressed because one or more lines are too long
|
|
@ -1,7 +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 'package:klimatologiot/blocs/authentication_bloc/authentication_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/auth/views/welcome_screen.dart';
|
||||||
import 'package:klimatologiot/screens/home/views/home_screen.dart';
|
import 'package:klimatologiot/screens/home/views/home_screen.dart';
|
||||||
|
|
||||||
|
|
@ -13,6 +13,15 @@ class MyAppView extends StatelessWidget {
|
||||||
return MaterialApp(
|
return MaterialApp(
|
||||||
title: 'Klimatologi',
|
title: 'Klimatologi',
|
||||||
debugShowCheckedModeBanner: false,
|
debugShowCheckedModeBanner: false,
|
||||||
|
localizationsDelegates: const [
|
||||||
|
GlobalMaterialLocalizations.delegate,
|
||||||
|
GlobalWidgetsLocalizations.delegate,
|
||||||
|
GlobalCupertinoLocalizations.delegate,
|
||||||
|
],
|
||||||
|
supportedLocales: const [
|
||||||
|
Locale('id', 'ID'),
|
||||||
|
Locale('en'),
|
||||||
|
],
|
||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
colorScheme: ColorScheme.light(
|
colorScheme: ColorScheme.light(
|
||||||
surface: Colors.grey.shade100,
|
surface: Colors.grey.shade100,
|
||||||
|
|
|
||||||
|
|
@ -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<void> 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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import 'dart:typed_data';
|
||||||
|
|
||||||
|
// Stub — tidak pernah dipanggil langsung, digantikan web atau mobile
|
||||||
|
Future<void> saveAndShareExcel(Uint8List bytes, String fileName) async {
|
||||||
|
throw UnsupportedError('Platform tidak didukung');
|
||||||
|
}
|
||||||
|
|
@ -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<void> 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);
|
||||||
|
}
|
||||||
|
|
@ -8,10 +8,12 @@
|
||||||
// share_plus: ^10.0.2
|
// share_plus: ^10.0.2
|
||||||
// ===========================================================
|
// ===========================================================
|
||||||
|
|
||||||
import 'dart:io';
|
import 'dart:typed_data';
|
||||||
import 'package:excel/excel.dart';
|
import 'package:excel/excel.dart';
|
||||||
|
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:monitoring_repository/monitoring_repository.dart';
|
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||||
|
import 'dart:io';
|
||||||
import 'package:path_provider/path_provider.dart';
|
import 'package:path_provider/path_provider.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,38 +19,32 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
final NotificationBloc _notificationBloc;
|
final NotificationBloc _notificationBloc;
|
||||||
StreamSubscription<MyWindSpeed>? _subscription;
|
StreamSubscription<MyWindSpeed>? _subscription;
|
||||||
|
|
||||||
WindSpeedBloc(
|
WindSpeedBloc({
|
||||||
{required MonitoringRepository repository,
|
required MonitoringRepository repository,
|
||||||
required NotificationBloc notificationBloc})
|
required NotificationBloc notificationBloc,
|
||||||
: _repository = repository,
|
}) : _repository = repository,
|
||||||
_notificationBloc = notificationBloc,
|
_notificationBloc = notificationBloc,
|
||||||
super(const WindSpeedState()) {
|
super(const WindSpeedState()) {
|
||||||
/// 🔥 START MONITORING
|
|
||||||
on<WatchWindSpeedStarted>(_onStarted, transformer: restartable());
|
on<WatchWindSpeedStarted>(_onStarted, transformer: restartable());
|
||||||
|
|
||||||
/// 🔥 REALTIME UPDATE
|
|
||||||
on<_WindSpeedRealtimeUpdated>(_onRealtimeUpdated);
|
on<_WindSpeedRealtimeUpdated>(_onRealtimeUpdated);
|
||||||
|
|
||||||
/// 🔥 CHANGE PERIOD
|
|
||||||
on<WindSpeedPeriodChanged>(_onPeriodChanged);
|
on<WindSpeedPeriodChanged>(_onPeriodChanged);
|
||||||
|
on<WindSpeedDateFilterChanged>(_onDateFilterChanged); // ← baru
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
// ════════════════════════════════════════════════════════════
|
||||||
/// 🚀 START
|
// START
|
||||||
/// =========================
|
// ════════════════════════════════════════════════════════════
|
||||||
Future<void> _onStarted(
|
Future<void> _onStarted(
|
||||||
WatchWindSpeedStarted event,
|
WatchWindSpeedStarted event,
|
||||||
Emitter<WindSpeedState> emit,
|
Emitter<WindSpeedState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(isLoading: true));
|
emit(state.copyWith(isLoading: true));
|
||||||
|
|
||||||
/// 1. Ambil history SEKALI
|
|
||||||
final history = await _repository.getSensorHistory(
|
final history = await _repository.getSensorHistory(
|
||||||
'anemometer/history',
|
'anemometer/history',
|
||||||
(json) => MyWindSpeed.fromJson(json),
|
(json) => MyWindSpeed.fromJson(json),
|
||||||
);
|
);
|
||||||
|
|
||||||
// ✅ Ini saja yang benar
|
|
||||||
final dailyGraph = TimeSeriesMapper.smooth(
|
final dailyGraph = TimeSeriesMapper.smooth(
|
||||||
TimeSeriesMapper.toDaily(
|
TimeSeriesMapper.toDaily(
|
||||||
data: history,
|
data: history,
|
||||||
|
|
@ -58,7 +52,6 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
getValue: (e) => e.speed,
|
getValue: (e) => e.speed,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final weekly = TimeSeriesMapper.smooth(
|
final weekly = TimeSeriesMapper.smooth(
|
||||||
TimeSeriesMapper.toWeekly(
|
TimeSeriesMapper.toWeekly(
|
||||||
data: history,
|
data: history,
|
||||||
|
|
@ -66,7 +59,6 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
getValue: (e) => e.speed,
|
getValue: (e) => e.speed,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
final monthly = TimeSeriesMapper.smooth(
|
final monthly = TimeSeriesMapper.smooth(
|
||||||
TimeSeriesMapper.toMonthly(
|
TimeSeriesMapper.toMonthly(
|
||||||
data: history,
|
data: history,
|
||||||
|
|
@ -75,25 +67,22 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Cek kondisi awal dari history
|
|
||||||
if (history.isNotEmpty) {
|
if (history.isNotEmpty) {
|
||||||
_emitWindAlert(history.last.speed);
|
_emitWindAlert(history.last.speed);
|
||||||
}
|
}
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
history: history,
|
history: history,
|
||||||
|
filteredHistory: history, // awal = semua data
|
||||||
dailySpeeds: dailyGraph,
|
dailySpeeds: dailyGraph,
|
||||||
weeklySpeeds: weekly,
|
weeklySpeeds: weekly,
|
||||||
monthlySpeeds: monthly,
|
monthlySpeeds: monthly,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
alertLevel: history.isNotEmpty // ← tambah ini
|
alertLevel:
|
||||||
? _getAlertLevel(history.last.speed)
|
history.isNotEmpty ? _getAlertLevel(history.last.speed) : 'Normal',
|
||||||
: "Normal",
|
|
||||||
));
|
));
|
||||||
|
|
||||||
/// 3. Start realtime stream (manual subscription)
|
|
||||||
await _subscription?.cancel();
|
await _subscription?.cancel();
|
||||||
|
|
||||||
_subscription = _repository
|
_subscription = _repository
|
||||||
.getSensorStream(
|
.getSensorStream(
|
||||||
'anemometer/realtime',
|
'anemometer/realtime',
|
||||||
|
|
@ -104,9 +93,9 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
// ════════════════════════════════════════════════════════════
|
||||||
/// ⚡ REALTIME UPDATE
|
// REALTIME UPDATE
|
||||||
/// =========================
|
// ════════════════════════════════════════════════════════════
|
||||||
void _onRealtimeUpdated(
|
void _onRealtimeUpdated(
|
||||||
_WindSpeedRealtimeUpdated event,
|
_WindSpeedRealtimeUpdated event,
|
||||||
Emitter<WindSpeedState> emit,
|
Emitter<WindSpeedState> emit,
|
||||||
|
|
@ -117,13 +106,11 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
|
|
||||||
if (index < updated.length) {
|
if (index < updated.length) {
|
||||||
final lastValue = updated[index];
|
final lastValue = updated[index];
|
||||||
|
|
||||||
/// 🔥 ANTI SPIKE + SMOOTHING
|
|
||||||
if (lastValue != 0) {
|
if (lastValue != 0) {
|
||||||
if ((newValue - lastValue).abs() > 15) {
|
if ((newValue - lastValue).abs() > 15) {
|
||||||
newValue = lastValue; // buang spike
|
newValue = lastValue;
|
||||||
} else {
|
} else {
|
||||||
newValue = (lastValue + newValue) / 2; // smoothing
|
newValue = (lastValue + newValue) / 2;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
updated[index] = newValue.clamp(0, 100);
|
updated[index] = newValue.clamp(0, 100);
|
||||||
|
|
@ -138,26 +125,24 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
// ════════════════════════════════════════════════════════════
|
||||||
/// 📊 CHANGE PERIOD
|
// PERIOD CHANGED
|
||||||
/// =========================
|
// ════════════════════════════════════════════════════════════
|
||||||
Future<void> _onPeriodChanged(
|
Future<void> _onPeriodChanged(
|
||||||
WindSpeedPeriodChanged event,
|
WindSpeedPeriodChanged event,
|
||||||
Emitter<WindSpeedState> emit,
|
Emitter<WindSpeedState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(selectedPeriod: event.period));
|
emit(state.copyWith(selectedPeriod: event.period));
|
||||||
|
|
||||||
final history = state.history;
|
final history = state.history;
|
||||||
|
|
||||||
List<double> raw;
|
List<double> raw;
|
||||||
|
if (event.period == 'Minggu Ini') {
|
||||||
if (event.period == "Minggu Ini") {
|
|
||||||
raw = TimeSeriesMapper.toWeekly(
|
raw = TimeSeriesMapper.toWeekly(
|
||||||
data: history,
|
data: history,
|
||||||
getTime: (e) => e.timestamp,
|
getTime: (e) => e.timestamp,
|
||||||
getValue: (e) => e.speed,
|
getValue: (e) => e.speed,
|
||||||
);
|
);
|
||||||
} else if (event.period == "Bulan Ini") {
|
} else if (event.period == 'Bulan Ini') {
|
||||||
raw = TimeSeriesMapper.toMonthly(
|
raw = TimeSeriesMapper.toMonthly(
|
||||||
data: history,
|
data: history,
|
||||||
getTime: (e) => e.timestamp,
|
getTime: (e) => e.timestamp,
|
||||||
|
|
@ -171,35 +156,60 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 🔥 baru di-smooth
|
|
||||||
final updatedGraph = TimeSeriesMapper.smooth(raw);
|
final updatedGraph = TimeSeriesMapper.smooth(raw);
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
dailySpeeds:
|
dailySpeeds:
|
||||||
event.period == "Hari Ini" ? updatedGraph : state.dailySpeeds,
|
event.period == 'Hari Ini' ? updatedGraph : state.dailySpeeds,
|
||||||
weeklySpeeds:
|
weeklySpeeds:
|
||||||
event.period == "Minggu Ini" ? updatedGraph : state.weeklySpeeds,
|
event.period == 'Minggu Ini' ? updatedGraph : state.weeklySpeeds,
|
||||||
monthlySpeeds:
|
monthlySpeeds:
|
||||||
event.period == "Bulan Ini" ? updatedGraph : state.monthlySpeeds,
|
event.period == 'Bulan Ini' ? updatedGraph : state.monthlySpeeds,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
// ════════════════════════════════════════════════════════════
|
||||||
Future<void> close() async {
|
// DATE FILTER CHANGED ← baru
|
||||||
await _subscription?.cancel();
|
// ════════════════════════════════════════════════════════════
|
||||||
return super.close();
|
void _onDateFilterChanged(
|
||||||
|
WindSpeedDateFilterChanged event,
|
||||||
|
Emitter<WindSpeedState> 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) {
|
String _getAlertLevel(double speed) {
|
||||||
if (speed >= _kWindDanger) return "Bahaya";
|
if (speed >= _kWindDanger) return 'Bahaya';
|
||||||
if (speed >= _kWindWarning) return "Waspada";
|
if (speed >= _kWindWarning) return 'Waspada';
|
||||||
return "Normal";
|
return 'Normal';
|
||||||
}
|
}
|
||||||
|
|
||||||
// =========================
|
|
||||||
// HELPER: kirim alert ke NotificationBloc
|
|
||||||
// =========================
|
|
||||||
void _emitWindAlert(double speed) {
|
void _emitWindAlert(double speed) {
|
||||||
final AlertSeverity severity;
|
final AlertSeverity severity;
|
||||||
final String message;
|
final String message;
|
||||||
|
|
@ -211,7 +221,7 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
severity = AlertSeverity.warning;
|
severity = AlertSeverity.warning;
|
||||||
message = 'Kecepatan angin ${speed.toStringAsFixed(1)} m/s — waspada';
|
message = 'Kecepatan angin ${speed.toStringAsFixed(1)} m/s — waspada';
|
||||||
} else {
|
} else {
|
||||||
severity = AlertSeverity.info; // normal → bersihkan alert
|
severity = AlertSeverity.info;
|
||||||
message = '';
|
message = '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -225,16 +235,19 @@ class WindSpeedBloc extends Bloc<WindSpeedEvent, WindSpeedState> {
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> close() async {
|
||||||
|
await _subscription?.cancel();
|
||||||
|
return super.close();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// =========================
|
/// Internal event — tidak diekspos ke luar
|
||||||
/// 🔒 INTERNAL EVENT (PRIVATE)
|
|
||||||
/// =========================
|
|
||||||
class _WindSpeedRealtimeUpdated extends WindSpeedEvent {
|
class _WindSpeedRealtimeUpdated extends WindSpeedEvent {
|
||||||
final MyWindSpeed data;
|
final MyWindSpeed data;
|
||||||
|
|
||||||
const _WindSpeedRealtimeUpdated(this.data);
|
const _WindSpeedRealtimeUpdated(this.data);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [data];
|
List<Object?> get props => [data];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,32 @@
|
||||||
part of 'wind_speed_bloc.dart';
|
part of 'wind_speed_bloc.dart';
|
||||||
|
|
||||||
sealed class WindSpeedEvent extends Equatable {
|
abstract class WindSpeedEvent extends Equatable {
|
||||||
const WindSpeedEvent();
|
const WindSpeedEvent();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [];
|
List<Object?> get props => [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perintah untuk mulai dengerin data Firebase
|
/// Mulai watch stream realtime + ambil history
|
||||||
class WatchWindSpeedStarted extends WindSpeedEvent {}
|
class WatchWindSpeedStarted extends WindSpeedEvent {
|
||||||
|
const WatchWindSpeedStarted();
|
||||||
|
}
|
||||||
|
|
||||||
// Perintah kalau user ganti filter (Hari Ini, Minggu Ini, dll)
|
/// Ganti periode grafik: "Hari Ini" | "Minggu Ini" | "Bulan Ini"
|
||||||
class WindSpeedPeriodChanged extends WindSpeedEvent {
|
class WindSpeedPeriodChanged extends WindSpeedEvent {
|
||||||
final String period;
|
final String period;
|
||||||
const WindSpeedPeriodChanged(this.period);
|
const WindSpeedPeriodChanged(this.period);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [period];
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Filter history berdasarkan tanggal.
|
||||||
|
/// Kirim [date] = null untuk reset (tampilkan semua)
|
||||||
|
class WindSpeedDateFilterChanged extends WindSpeedEvent {
|
||||||
|
final DateTime? date;
|
||||||
|
const WindSpeedDateFilterChanged(this.date);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [date];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,57 +1,73 @@
|
||||||
part of 'wind_speed_bloc.dart';
|
part of 'wind_speed_bloc.dart';
|
||||||
|
|
||||||
class WindSpeedState extends Equatable {
|
class WindSpeedState extends Equatable {
|
||||||
|
final bool isLoading;
|
||||||
final double currentSpeed;
|
final double currentSpeed;
|
||||||
|
final String alertLevel;
|
||||||
final String selectedPeriod;
|
final String selectedPeriod;
|
||||||
|
|
||||||
|
// ── Graf ─────────────────────────────────────────────────────
|
||||||
final List<double> dailySpeeds;
|
final List<double> dailySpeeds;
|
||||||
final List<double> weeklySpeeds;
|
final List<double> weeklySpeeds;
|
||||||
final List<double> monthlySpeeds;
|
final List<double> monthlySpeeds;
|
||||||
final bool isLoading;
|
|
||||||
final List<MyWindSpeed> history;
|
// ── History ──────────────────────────────────────────────────
|
||||||
final String alertLevel; // "Normal" | "Waspada" | "Bahaya"
|
final List<MyWindSpeed> history; // semua data mentah
|
||||||
|
final List<MyWindSpeed> filteredHistory; // setelah difilter tanggal
|
||||||
|
final DateTime? selectedDate; // null = tampilkan semua
|
||||||
|
|
||||||
const WindSpeedState({
|
const WindSpeedState({
|
||||||
|
this.isLoading = false,
|
||||||
this.currentSpeed = 0.0,
|
this.currentSpeed = 0.0,
|
||||||
this.selectedPeriod = "Hari Ini",
|
this.alertLevel = 'Normal',
|
||||||
|
this.selectedPeriod = 'Hari Ini',
|
||||||
this.dailySpeeds = const [],
|
this.dailySpeeds = const [],
|
||||||
this.isLoading = true,
|
|
||||||
this.history = const [],
|
|
||||||
this.monthlySpeeds = const [],
|
|
||||||
this.weeklySpeeds = const [],
|
this.weeklySpeeds = const [],
|
||||||
this.alertLevel = "Normal",
|
this.monthlySpeeds = const [],
|
||||||
|
this.history = const [],
|
||||||
|
this.filteredHistory = const [],
|
||||||
|
this.selectedDate,
|
||||||
});
|
});
|
||||||
|
|
||||||
WindSpeedState copyWith({
|
WindSpeedState copyWith({
|
||||||
|
bool? isLoading,
|
||||||
double? currentSpeed,
|
double? currentSpeed,
|
||||||
|
String? alertLevel,
|
||||||
String? selectedPeriod,
|
String? selectedPeriod,
|
||||||
List<double>? dailySpeeds,
|
List<double>? dailySpeeds,
|
||||||
List<double>? weeklySpeeds,
|
List<double>? weeklySpeeds,
|
||||||
List<double>? monthlySpeeds,
|
List<double>? monthlySpeeds,
|
||||||
bool? isLoading,
|
|
||||||
List<MyWindSpeed>? history,
|
List<MyWindSpeed>? history,
|
||||||
String? alertLevel,
|
List<MyWindSpeed>? filteredHistory,
|
||||||
|
DateTime? selectedDate,
|
||||||
|
bool clearSelectedDate = false,
|
||||||
}) {
|
}) {
|
||||||
return WindSpeedState(
|
return WindSpeedState(
|
||||||
|
isLoading: isLoading ?? this.isLoading,
|
||||||
currentSpeed: currentSpeed ?? this.currentSpeed,
|
currentSpeed: currentSpeed ?? this.currentSpeed,
|
||||||
|
alertLevel: alertLevel ?? this.alertLevel,
|
||||||
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
||||||
dailySpeeds: dailySpeeds ?? this.dailySpeeds,
|
dailySpeeds: dailySpeeds ?? this.dailySpeeds,
|
||||||
weeklySpeeds: weeklySpeeds ?? this.weeklySpeeds,
|
weeklySpeeds: weeklySpeeds ?? this.weeklySpeeds,
|
||||||
monthlySpeeds: monthlySpeeds ?? this.monthlySpeeds,
|
monthlySpeeds: monthlySpeeds ?? this.monthlySpeeds,
|
||||||
isLoading: isLoading ?? this.isLoading,
|
|
||||||
history: history ?? this.history,
|
history: history ?? this.history,
|
||||||
alertLevel: alertLevel ?? this.alertLevel,
|
filteredHistory: filteredHistory ?? this.filteredHistory,
|
||||||
|
selectedDate:
|
||||||
|
clearSelectedDate ? null : (selectedDate ?? this.selectedDate),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [
|
List<Object?> get props => [
|
||||||
|
isLoading,
|
||||||
currentSpeed,
|
currentSpeed,
|
||||||
|
alertLevel,
|
||||||
selectedPeriod,
|
selectedPeriod,
|
||||||
dailySpeeds,
|
dailySpeeds,
|
||||||
weeklySpeeds,
|
weeklySpeeds,
|
||||||
monthlySpeeds,
|
monthlySpeeds,
|
||||||
isLoading,
|
|
||||||
history,
|
history,
|
||||||
alertLevel,
|
filteredHistory,
|
||||||
|
selectedDate,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,121 @@
|
||||||
|
// ===========================================================
|
||||||
|
// wind_speed_screen.dart
|
||||||
|
// Lokasi: lib/screens/monitoring/wind_speed/views/
|
||||||
|
// ===========================================================
|
||||||
|
|
||||||
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 'package:intl/intl.dart';
|
||||||
|
import 'package:monitoring_repository/monitoring_repository.dart';
|
||||||
import '../blocs/wind_speed_bloc.dart';
|
import '../blocs/wind_speed_bloc.dart';
|
||||||
import './widgets/wind_speed_chart_widget.dart';
|
import 'widgets/wind_speed_chart_widget.dart';
|
||||||
import 'widgets/period_selector.dart';
|
import 'widgets/period_selector.dart';
|
||||||
|
|
||||||
import '../../shared/utils/pdf/pdf_export_service.dart';
|
|
||||||
import '../../shared/widgets/export_pdf_button.dart';
|
|
||||||
import '../../device_setup/views/device_setup_screen.dart';
|
import '../../device_setup/views/device_setup_screen.dart';
|
||||||
|
import '../../shared/utils/excel/wind_speed_excel_service.dart';
|
||||||
|
|
||||||
class WindSpeedScreen extends StatelessWidget {
|
class WindSpeedScreen extends StatefulWidget {
|
||||||
const WindSpeedScreen({super.key});
|
const WindSpeedScreen({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<WindSpeedScreen> createState() => _WindSpeedScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _WindSpeedScreenState extends State<WindSpeedScreen> {
|
||||||
|
// ── Date picker ─────────────────────────────────────────────
|
||||||
|
Future<void> _pickDate(BuildContext context, WindSpeedState state) async {
|
||||||
|
DateTime firstDate = DateTime.now().subtract(const Duration(days: 365));
|
||||||
|
DateTime lastDate = DateTime.now();
|
||||||
|
|
||||||
|
if (state.history.isNotEmpty) {
|
||||||
|
final sorted = [...state.history]
|
||||||
|
..sort((a, b) => a.timestamp.compareTo(b.timestamp));
|
||||||
|
firstDate = DateTime(sorted.first.timestamp.year,
|
||||||
|
sorted.first.timestamp.month, sorted.first.timestamp.day);
|
||||||
|
lastDate = DateTime(sorted.last.timestamp.year,
|
||||||
|
sorted.last.timestamp.month, sorted.last.timestamp.day);
|
||||||
|
}
|
||||||
|
|
||||||
|
final picked = await showDatePicker(
|
||||||
|
context: context,
|
||||||
|
initialDate: state.selectedDate ?? lastDate,
|
||||||
|
firstDate: firstDate,
|
||||||
|
lastDate: lastDate,
|
||||||
|
locale: const Locale('id', 'ID'),
|
||||||
|
builder: (context, child) => Theme(
|
||||||
|
data: Theme.of(context).copyWith(
|
||||||
|
colorScheme: ColorScheme.light(
|
||||||
|
primary: Colors.blue.shade700,
|
||||||
|
onPrimary: Colors.white,
|
||||||
|
surface: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: child!,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (picked != null && context.mounted) {
|
||||||
|
context.read<WindSpeedBloc>().add(WindSpeedDateFilterChanged(picked));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Export Excel ─────────────────────────────────────────────
|
||||||
|
Future<void> _exportExcel(BuildContext context, WindSpeedState state) async {
|
||||||
|
final messenger = ScaffoldMessenger.of(context);
|
||||||
|
try {
|
||||||
|
messenger.showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Row(children: [
|
||||||
|
SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2, color: Colors.white)),
|
||||||
|
SizedBox(width: 12),
|
||||||
|
Text('Membuat file Excel...'),
|
||||||
|
]),
|
||||||
|
duration: Duration(seconds: 30),
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
await WindSpeedExcelService.export(
|
||||||
|
currentSpeed: state.currentSpeed,
|
||||||
|
alertLevel: state.alertLevel,
|
||||||
|
period: state.selectedPeriod,
|
||||||
|
history: state.history,
|
||||||
|
filterDate: state.selectedDate,
|
||||||
|
);
|
||||||
|
|
||||||
|
messenger.hideCurrentSnackBar();
|
||||||
|
messenger.showSnackBar(SnackBar(
|
||||||
|
content: const Row(children: [
|
||||||
|
Icon(Icons.check_circle, color: Colors.white),
|
||||||
|
SizedBox(width: 8),
|
||||||
|
Text('File Excel berhasil dibuat!'),
|
||||||
|
]),
|
||||||
|
backgroundColor: Colors.green.shade600,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
duration: const Duration(seconds: 3),
|
||||||
|
));
|
||||||
|
} catch (e) {
|
||||||
|
messenger.hideCurrentSnackBar();
|
||||||
|
messenger.showSnackBar(SnackBar(
|
||||||
|
content: Text('Gagal: $e'),
|
||||||
|
backgroundColor: Colors.red.shade600,
|
||||||
|
behavior: SnackBarBehavior.floating,
|
||||||
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: Colors.grey.shade100,
|
backgroundColor: Colors.grey.shade100,
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: const Text(
|
title: const Text(
|
||||||
"Kecepatan Angin",
|
'Kecepatan Angin',
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
),
|
),
|
||||||
centerTitle: true,
|
centerTitle: true,
|
||||||
|
|
@ -25,79 +123,70 @@ class WindSpeedScreen extends StatelessWidget {
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
foregroundColor: Colors.black,
|
foregroundColor: Colors.black,
|
||||||
actions: [
|
actions: [
|
||||||
|
BlocBuilder<WindSpeedBloc, WindSpeedState>(
|
||||||
|
builder: (context, state) => IconButton(
|
||||||
|
tooltip: 'Export Excel',
|
||||||
|
icon: const Icon(Icons.file_download_outlined),
|
||||||
|
onPressed: state.history.isEmpty
|
||||||
|
? null
|
||||||
|
: () => _exportExcel(context, state),
|
||||||
|
),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
tooltip: 'Pengaturan Perangkat',
|
tooltip: 'Pengaturan Perangkat',
|
||||||
icon: const Icon(Icons.settings_rounded),
|
icon: const Icon(Icons.settings_rounded),
|
||||||
onPressed: () {
|
onPressed: () => Navigator.push(
|
||||||
Navigator.push(
|
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(builder: (_) => const DeviceSetupScreen()),
|
||||||
builder: (_) => const DeviceSetupScreen(),
|
|
||||||
),
|
),
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
/// === Body area, lokasi dan tata letak Widgets === ///
|
|
||||||
body: BlocBuilder<WindSpeedBloc, WindSpeedState>(
|
body: BlocBuilder<WindSpeedBloc, WindSpeedState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
if (state.isLoading) {
|
if (state.isLoading) {
|
||||||
return const Center(child: CircularProgressIndicator());
|
return const Center(child: CircularProgressIndicator());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Pilih data sesuai periode
|
|
||||||
final List<double> data = switch (state.selectedPeriod) {
|
final List<double> data = switch (state.selectedPeriod) {
|
||||||
"Minggu Ini" => state.weeklySpeeds,
|
'Minggu Ini' => state.weeklySpeeds,
|
||||||
"Bulan Ini" => state.monthlySpeeds,
|
'Bulan Ini' => state.monthlySpeeds,
|
||||||
_ => state.dailySpeeds,
|
_ => state.dailySpeeds,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Konversi history MyWindSpeed → Map (untuk tabel PDF)
|
|
||||||
final historyMaps = state.history
|
|
||||||
.map((e) => {
|
|
||||||
'timestamp': e.timestamp,
|
|
||||||
'speed': e.speed,
|
|
||||||
})
|
|
||||||
.toList();
|
|
||||||
|
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(20.0),
|
padding: const EdgeInsets.all(20.0),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
/// ==== 1. Tampilan Angka Utama kecepatan angin (Hero Widget look) ==== ///
|
// ── 1. Hero kecepatan ────────────────────────────
|
||||||
_buildMainSpeedDisplay(state.currentSpeed),
|
_buildMainSpeedDisplay(state.currentSpeed, state.alertLevel),
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
const Text("Tren Kecepatan",
|
// ── 2. Tren grafik ───────────────────────────────
|
||||||
style:
|
const Text(
|
||||||
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
|
'Tren Kecepatan',
|
||||||
const SizedBox(height: 15),
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
const PeriodSelector(),
|
const PeriodSelector(),
|
||||||
const SizedBox(height: 15),
|
const SizedBox(height: 12),
|
||||||
|
if (data.isEmpty)
|
||||||
|
_buildEmptyChart()
|
||||||
|
else
|
||||||
WindSpeedChartWidget(
|
WindSpeedChartWidget(
|
||||||
dailySpeeds: data,
|
dailySpeeds: data,
|
||||||
period: state.selectedPeriod,
|
period: state.selectedPeriod,
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
const SizedBox(height: 30),
|
// ── 3. Info status + periode ─────────────────────
|
||||||
|
|
||||||
// 3. Info Tambahan (Status/Periode)
|
|
||||||
_buildDetailRow(state.selectedPeriod, state.alertLevel),
|
_buildDetailRow(state.selectedPeriod, state.alertLevel),
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 28),
|
||||||
|
|
||||||
// ← Tombol Export PDF
|
// ── 4. Riwayat data ──────────────────────────────
|
||||||
ExportPdfButton(
|
_buildHistorySection(context, state),
|
||||||
onExport: () => PdfExportService.windSpeed(
|
const SizedBox(height: 20),
|
||||||
currentSpeed: state.currentSpeed,
|
|
||||||
period: state.selectedPeriod,
|
|
||||||
speeds: data,
|
|
||||||
timestamp: DateTime.now(),
|
|
||||||
historyData: historyMaps.isNotEmpty ? historyMaps : null,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
@ -106,10 +195,19 @@ class WindSpeedScreen extends StatelessWidget {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildMainSpeedDisplay(double speed) {
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// Hero kecepatan
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Widget _buildMainSpeedDisplay(double speed, String alertLevel) {
|
||||||
|
final (badgeColor, badgeIcon) = switch (alertLevel) {
|
||||||
|
'Bahaya' => (Colors.red.shade400, Icons.warning_rounded),
|
||||||
|
'Waspada' => (Colors.orange.shade400, Icons.info_outline_rounded),
|
||||||
|
_ => (Colors.green.shade400, Icons.check_circle_outline_rounded),
|
||||||
|
};
|
||||||
|
|
||||||
return Container(
|
return Container(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 40),
|
padding: const EdgeInsets.symmetric(vertical: 36),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
gradient: LinearGradient(
|
gradient: LinearGradient(
|
||||||
colors: [Colors.blue.shade400, Colors.blue.shade800],
|
colors: [Colors.blue.shade400, Colors.blue.shade800],
|
||||||
|
|
@ -122,38 +220,73 @@ class WindSpeedScreen extends StatelessWidget {
|
||||||
color: Colors.blue.withValues(alpha: 0.3),
|
color: Colors.blue.withValues(alpha: 0.3),
|
||||||
blurRadius: 20,
|
blurRadius: 20,
|
||||||
offset: const Offset(0, 10),
|
offset: const Offset(0, 10),
|
||||||
)
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.air, color: Colors.white, size: 50),
|
const Icon(Icons.air, color: Colors.white, size: 46),
|
||||||
const SizedBox(
|
const SizedBox(height: 8),
|
||||||
height: 10,
|
|
||||||
),
|
|
||||||
Text(
|
Text(
|
||||||
"${speed.toStringAsFixed(1)}",
|
speed.toStringAsFixed(1),
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 80, fontWeight: FontWeight.bold, color: Colors.white),
|
fontSize: 80,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
height: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text('m/s',
|
||||||
|
style: TextStyle(fontSize: 20, color: Colors.white70)),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${(speed * 3.6).toStringAsFixed(1)} km/h',
|
||||||
|
style: const TextStyle(fontSize: 14, color: Colors.white54),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 14),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 7),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: badgeColor.withValues(alpha: 0.2),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: badgeColor.withValues(alpha: 0.7)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(badgeIcon, color: badgeColor, size: 15),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
alertLevel,
|
||||||
|
style: TextStyle(
|
||||||
|
color: badgeColor,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Text("m/s",
|
|
||||||
style: TextStyle(fontSize: 20, color: Colors.white70))
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// Info baris: status + periode
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
Widget _buildDetailRow(String period, String alertLevel) {
|
Widget _buildDetailRow(String period, String alertLevel) {
|
||||||
final (icon, color) = switch (alertLevel) {
|
final (icon, color) = switch (alertLevel) {
|
||||||
"Bahaya" => (Icons.warning_rounded, Colors.red),
|
'Bahaya' => (Icons.warning_rounded, Colors.red),
|
||||||
"Waspada" => (Icons.info_outline, Colors.orange),
|
'Waspada' => (Icons.info_outline, Colors.orange),
|
||||||
_ => (Icons.check_circle, Colors.green),
|
_ => (Icons.check_circle, Colors.green),
|
||||||
};
|
};
|
||||||
|
|
||||||
return Row(
|
return Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
_miniInfoCard("Status", alertLevel, icon, color),
|
_miniInfoCard('Status', alertLevel, icon, color),
|
||||||
_miniInfoCard("Periode", period, Icons.timer, Colors.orange),
|
_miniInfoCard('Periode', period, Icons.timer, Colors.orange),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
@ -165,19 +298,340 @@ class WindSpeedScreen extends StatelessWidget {
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(color: Colors.black.withValues(alpha: 0.05), blurRadius: 6),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: color, size: 30),
|
Icon(icon, color: color, size: 28),
|
||||||
const SizedBox(width: 10),
|
const SizedBox(width: 10),
|
||||||
Column(
|
Expanded(
|
||||||
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(label,
|
Text(label,
|
||||||
style: const TextStyle(color: Colors.grey, fontSize: 12)),
|
style: const TextStyle(color: Colors.grey, fontSize: 11)),
|
||||||
Text(value, style: const TextStyle(fontWeight: FontWeight.bold)),
|
Text(
|
||||||
|
value,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontWeight: FontWeight.bold, fontSize: 13),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// Placeholder chart kosong — cegah crash saat data belum ada
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Widget _buildEmptyChart() {
|
||||||
|
return Container(
|
||||||
|
height: 160,
|
||||||
|
width: double.infinity,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(Icons.bar_chart_rounded, size: 42, color: Colors.grey.shade300),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
'Data grafik belum tersedia',
|
||||||
|
style: TextStyle(color: Colors.grey.shade400, fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// Section riwayat
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
Widget _buildHistorySection(BuildContext context, WindSpeedState state) {
|
||||||
|
// Urutkan terbaru di atas
|
||||||
|
final sorted = [...state.filteredHistory]
|
||||||
|
..sort((a, b) => b.timestamp.compareTo(a.timestamp));
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Header judul + tombol filter
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Riwayat Data',
|
||||||
|
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
const Spacer(),
|
||||||
|
state.selectedDate != null
|
||||||
|
? _FilterChip(
|
||||||
|
label: DateFormat('dd MMM yyyy', 'id_ID')
|
||||||
|
.format(state.selectedDate!),
|
||||||
|
icon: Icons.close_rounded,
|
||||||
|
onTap: () => context
|
||||||
|
.read<WindSpeedBloc>()
|
||||||
|
.add(const WindSpeedDateFilterChanged(null)),
|
||||||
)
|
)
|
||||||
|
: _FilterChip(
|
||||||
|
label: 'Filter Tanggal',
|
||||||
|
icon: Icons.calendar_month_rounded,
|
||||||
|
onTap: () => _pickDate(context, state),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
if (state.selectedDate != null) ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${sorted.length} data pada ${DateFormat('dd MMMM yyyy', 'id_ID').format(state.selectedDate!)}',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'${sorted.length} data tersimpan',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey.shade500),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
// List dengan batas tinggi 420px — di dalamnya bisa di-scroll
|
||||||
|
sorted.isEmpty
|
||||||
|
? _buildEmptyHistory(state.selectedDate != null)
|
||||||
|
: ConstrainedBox(
|
||||||
|
constraints: const BoxConstraints(maxHeight: 420),
|
||||||
|
child: Container(
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: Colors.black.withValues(alpha: 0.05),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
child: ListView.separated(
|
||||||
|
shrinkWrap: false,
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
itemCount: sorted.length,
|
||||||
|
separatorBuilder: (_, __) =>
|
||||||
|
Divider(height: 1, color: Colors.grey.shade100),
|
||||||
|
itemBuilder: (context, i) =>
|
||||||
|
_HistoryTile(item: sorted[i], index: i),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildEmptyHistory(bool hasFilter) {
|
||||||
|
return Container(
|
||||||
|
width: double.infinity,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 36),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
hasFilter ? Icons.search_off_rounded : Icons.inbox_rounded,
|
||||||
|
size: 44,
|
||||||
|
color: Colors.grey.shade300,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Text(
|
||||||
|
hasFilter
|
||||||
|
? 'Tidak ada data untuk tanggal ini'
|
||||||
|
: 'Belum ada data history',
|
||||||
|
style: TextStyle(color: Colors.grey.shade400, fontSize: 13),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// Chip filter tanggal
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
class _FilterChip extends StatelessWidget {
|
||||||
|
final String label;
|
||||||
|
final IconData icon;
|
||||||
|
final VoidCallback onTap;
|
||||||
|
|
||||||
|
const _FilterChip(
|
||||||
|
{required this.label, required this.icon, required this.onTap});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final color = Colors.blue.shade700;
|
||||||
|
return InkWell(
|
||||||
|
onTap: onTap,
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
child: Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 7),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: color.withValues(alpha: 0.08),
|
||||||
|
borderRadius: BorderRadius.circular(20),
|
||||||
|
border: Border.all(color: color.withValues(alpha: 0.35)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Icon(icon, size: 14, color: color),
|
||||||
|
const SizedBox(width: 6),
|
||||||
|
Text(
|
||||||
|
label,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12, color: color, fontWeight: FontWeight.w600),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
// Satu baris data riwayat
|
||||||
|
// ════════════════════════════════════════════════════════════
|
||||||
|
class _HistoryTile extends StatelessWidget {
|
||||||
|
final MyWindSpeed item;
|
||||||
|
final int index;
|
||||||
|
|
||||||
|
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';
|
||||||
|
return 'Normal';
|
||||||
|
}
|
||||||
|
|
||||||
|
Color get _alertColor {
|
||||||
|
switch (_alertLevel) {
|
||||||
|
case 'Bahaya':
|
||||||
|
return Colors.red.shade600;
|
||||||
|
case 'Waspada':
|
||||||
|
return Colors.orange.shade700;
|
||||||
|
default:
|
||||||
|
return Colors.green.shade600;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconData get _alertIcon {
|
||||||
|
switch (_alertLevel) {
|
||||||
|
case 'Bahaya':
|
||||||
|
return Icons.warning_rounded;
|
||||||
|
case 'Waspada':
|
||||||
|
return Icons.info_outline_rounded;
|
||||||
|
default:
|
||||||
|
return Icons.check_circle_outline_rounded;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final kmh = item.speed * 3.6;
|
||||||
|
final bg = index.isEven ? Colors.grey.shade50 : Colors.white;
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
color: bg,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||||
|
child: Row(
|
||||||
|
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),
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 11,
|
||||||
|
color: Colors.grey.shade500,
|
||||||
|
fontFamily: 'monospace'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,14 @@
|
||||||
|
|
||||||
#include "generated_plugin_registrant.h"
|
#include "generated_plugin_registrant.h"
|
||||||
|
|
||||||
|
#include <flutter_localization/flutter_localization_plugin.h>
|
||||||
#include <printing/printing_plugin.h>
|
#include <printing/printing_plugin.h>
|
||||||
#include <url_launcher_linux/url_launcher_plugin.h>
|
#include <url_launcher_linux/url_launcher_plugin.h>
|
||||||
|
|
||||||
void fl_register_plugins(FlPluginRegistry* registry) {
|
void fl_register_plugins(FlPluginRegistry* registry) {
|
||||||
|
g_autoptr(FlPluginRegistrar) flutter_localization_registrar =
|
||||||
|
fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterLocalizationPlugin");
|
||||||
|
flutter_localization_plugin_register_with_registrar(flutter_localization_registrar);
|
||||||
g_autoptr(FlPluginRegistrar) printing_registrar =
|
g_autoptr(FlPluginRegistrar) printing_registrar =
|
||||||
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
|
fl_plugin_registry_get_registrar_for_plugin(registry, "PrintingPlugin");
|
||||||
printing_plugin_register_with_registrar(printing_registrar);
|
printing_plugin_register_with_registrar(printing_registrar);
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,7 @@
|
||||||
#
|
#
|
||||||
|
|
||||||
list(APPEND FLUTTER_PLUGIN_LIST
|
list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
|
flutter_localization
|
||||||
printing
|
printing
|
||||||
url_launcher_linux
|
url_launcher_linux
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,11 @@ import cloud_firestore
|
||||||
import firebase_auth
|
import firebase_auth
|
||||||
import firebase_core
|
import firebase_core
|
||||||
import firebase_database
|
import firebase_database
|
||||||
|
import flutter_localization
|
||||||
import google_sign_in_ios
|
import google_sign_in_ios
|
||||||
import printing
|
import printing
|
||||||
import share_plus
|
import share_plus
|
||||||
|
import shared_preferences_foundation
|
||||||
|
|
||||||
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
AppSettingsPlugin.register(with: registry.registrar(forPlugin: "AppSettingsPlugin"))
|
AppSettingsPlugin.register(with: registry.registrar(forPlugin: "AppSettingsPlugin"))
|
||||||
|
|
@ -20,7 +22,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||||
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"))
|
||||||
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
|
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
|
||||||
|
FlutterLocalizationPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalizationPlugin"))
|
||||||
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
|
||||||
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
PrintingPlugin.register(with: registry.registrar(forPlugin: "PrintingPlugin"))
|
||||||
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
|
||||||
|
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
|
||||||
}
|
}
|
||||||
|
|
|
||||||
77
pubspec.lock
77
pubspec.lock
|
|
@ -334,6 +334,19 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.2"
|
version: "3.0.2"
|
||||||
|
flutter_localization:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: flutter_localization
|
||||||
|
sha256: a670baf52f4c99859c07293092b9fbc6508235fc88232f5cbb772f2687610a32
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.4.0"
|
||||||
|
flutter_localizations:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
flutter_test:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|
@ -735,6 +748,62 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "7.1.0"
|
version: "7.1.0"
|
||||||
|
shared_preferences:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences
|
||||||
|
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.5"
|
||||||
|
shared_preferences_android:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_android
|
||||||
|
sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.23"
|
||||||
|
shared_preferences_foundation:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_foundation
|
||||||
|
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.5.6"
|
||||||
|
shared_preferences_linux:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_linux
|
||||||
|
sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
|
shared_preferences_platform_interface:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_platform_interface
|
||||||
|
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.2"
|
||||||
|
shared_preferences_web:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_web
|
||||||
|
sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.3"
|
||||||
|
shared_preferences_windows:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: shared_preferences_windows
|
||||||
|
sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.4.1"
|
||||||
simple_gesture_detector:
|
simple_gesture_detector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
@ -820,6 +889,14 @@ packages:
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.0"
|
||||||
|
universal_io:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: universal_io
|
||||||
|
sha256: f63cbc48103236abf48e345e07a03ce5757ea86285ed313a6a032596ed9301e2
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.3.1"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ dependencies:
|
||||||
flutter:
|
flutter:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
flutter_bloc: ^8.1.0
|
flutter_bloc: ^8.1.0
|
||||||
|
flutter_localization: ^0.4.0
|
||||||
google_fonts: ^8.0.2
|
google_fonts: ^8.0.2
|
||||||
google_sign_in: ^6.3.0
|
google_sign_in: ^6.3.0
|
||||||
intl: ^0.20.2
|
intl: ^0.20.2
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@
|
||||||
#include <cloud_firestore/cloud_firestore_plugin_c_api.h>
|
#include <cloud_firestore/cloud_firestore_plugin_c_api.h>
|
||||||
#include <firebase_auth/firebase_auth_plugin_c_api.h>
|
#include <firebase_auth/firebase_auth_plugin_c_api.h>
|
||||||
#include <firebase_core/firebase_core_plugin_c_api.h>
|
#include <firebase_core/firebase_core_plugin_c_api.h>
|
||||||
|
#include <flutter_localization/flutter_localization_plugin_c_api.h>
|
||||||
#include <printing/printing_plugin.h>
|
#include <printing/printing_plugin.h>
|
||||||
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
#include <share_plus/share_plus_windows_plugin_c_api.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
|
|
@ -20,6 +21,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi"));
|
registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi"));
|
||||||
FirebaseCorePluginCApiRegisterWithRegistrar(
|
FirebaseCorePluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
|
||||||
|
FlutterLocalizationPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("FlutterLocalizationPluginCApi"));
|
||||||
PrintingPluginRegisterWithRegistrar(
|
PrintingPluginRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
registry->GetRegistrarForPlugin("PrintingPlugin"));
|
||||||
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
SharePlusWindowsPluginCApiRegisterWithRegistrar(
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
||||||
cloud_firestore
|
cloud_firestore
|
||||||
firebase_auth
|
firebase_auth
|
||||||
firebase_core
|
firebase_core
|
||||||
|
flutter_localization
|
||||||
printing
|
printing
|
||||||
share_plus
|
share_plus
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue