Merge branch 'branch' of https://github.com/kleponijo/klimatologi into branch

This commit is contained in:
Mochamad ongki ramadani 2026-05-15 10:57:16 +07:00
commit bc9ca486c3
17 changed files with 1463 additions and 158 deletions

File diff suppressed because one or more lines are too long

View File

@ -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,

View File

@ -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,
);
}

View File

@ -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');
}

View File

@ -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);
}

View File

@ -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';

View File

@ -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];
} }

View File

@ -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];
} }

View File

@ -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,
]; ];
} }

View File

@ -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(builder: (_) => const DeviceSetupScreen()),
MaterialPageRoute( ),
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 Text("Tren Kecepatan",
style:
TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 15),
const PeriodSelector(),
const SizedBox(height: 15),
WindSpeedChartWidget(
dailySpeeds: data,
period: state.selectedPeriod,
),
const SizedBox(height: 30),
// 3. Info Tambahan (Status/Periode)
_buildDetailRow(state.selectedPeriod, state.alertLevel),
const SizedBox(height: 24), const SizedBox(height: 24),
// Tombol Export PDF // 2. Tren grafik
ExportPdfButton( const Text(
onExport: () => PdfExportService.windSpeed( 'Tren Kecepatan',
currentSpeed: state.currentSpeed, style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
period: state.selectedPeriod,
speeds: data,
timestamp: DateTime.now(),
historyData: historyMaps.isNotEmpty ? historyMaps : null,
),
), ),
const SizedBox(height: 12),
const PeriodSelector(),
const SizedBox(height: 12),
if (data.isEmpty)
_buildEmptyChart()
else
WindSpeedChartWidget(
dailySpeeds: data,
period: state.selectedPeriod,
),
const SizedBox(height: 20),
// 3. Info status + periode
_buildDetailRow(state.selectedPeriod, state.alertLevel),
const SizedBox(height: 28),
// 4. Riwayat data
_buildHistorySection(context, state),
const SizedBox(height: 20),
], ],
), ),
); );
@ -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(
crossAxisAlignment: CrossAxisAlignment.start, child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Text(label, children: [
style: const TextStyle(color: Colors.grey, fontSize: 12)), Text(label,
Text(value, style: const TextStyle(fontWeight: FontWeight.bold)), style: const TextStyle(color: Colors.grey, fontSize: 11)),
], 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),
),
],
),
),
], ],
), ),
); );

View File

@ -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);

View File

@ -3,6 +3,7 @@
# #
list(APPEND FLUTTER_PLUGIN_LIST list(APPEND FLUTTER_PLUGIN_LIST
flutter_localization
printing printing
url_launcher_linux url_launcher_linux
) )

View File

@ -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"))
} }

View File

@ -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:

View File

@ -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

View File

@ -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(

View File

@ -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