diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 360feb0..a783471 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -3,6 +3,8 @@ plugins {
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
+ // Firebase Cloud Messaging: legge android/app/google-services.json.
+ id("com.google.gms.google-services")
}
android {
@@ -11,6 +13,9 @@ android {
ndkVersion = flutter.ndkVersion
compileOptions {
+ // Richiesto da flutter_local_notifications (notifiche mostrate quando
+ // l'app e' aperta).
+ isCoreLibraryDesugaringEnabled = true
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
@@ -42,3 +47,8 @@ android {
flutter {
source = "../.."
}
+
+dependencies {
+ // Libreria di supporto richiesta dal desugaring (flutter_local_notifications).
+ coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.1.4")
+}
diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml
index 399f698..21b3144 100644
--- a/android/app/src/debug/AndroidManifest.xml
+++ b/android/app/src/debug/AndroidManifest.xml
@@ -4,4 +4,9 @@
to allow setting breakpoints, to provide hot reload, etc.
-->
+
+
+
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 0deed65..9884219 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -1,4 +1,8 @@
+
+
+
main() async {
WidgetsFlutterBinding.ensureInitialized();
await initializeDateFormatting('it_IT');
+
+ await PushService.initFirebase();
+ await PushService.enableForegroundNotifications();
runApp(
ChangeNotifierProvider(create: (_) => AppState(), child: const MyApp()),
);
diff --git a/lib/screens/account_page.dart b/lib/screens/account_page.dart
index 1e55583..c138d4e 100644
--- a/lib/screens/account_page.dart
+++ b/lib/screens/account_page.dart
@@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import '../models/school.dart';
+import '../services/push_api.dart';
import '../services/vanguard_api.dart';
import '../widgets/yogibook_background.dart';
@@ -41,12 +42,54 @@ class _AccountPageState extends State {
bool _obscurePw = true;
bool _obscurePw2 = true;
+ bool? _pushEnabled;
+ bool _savingPush = false;
+
final _picker = ImagePicker();
@override
void initState() {
super.initState();
_loadProfile();
+ _loadPushPreference();
+ }
+
+ Future _loadPushPreference() async {
+ try {
+ final attive = await PushApi.fetchPreference(token: widget.token);
+ if (!mounted) return;
+ setState(() => _pushEnabled = attive);
+ } catch (_) {
+ if (!mounted) return;
+ setState(() => _pushEnabled = null);
+ }
+ }
+
+ Future _togglePush(bool valore) async {
+ final precedente = _pushEnabled;
+
+ setState(() {
+ _pushEnabled = valore;
+ _savingPush = true;
+ });
+
+ try {
+ final salvato = await PushApi.updatePreference(
+ token: widget.token,
+ enabled: valore,
+ );
+ if (!mounted) return;
+ setState(() => _pushEnabled = salvato);
+ _snack(
+ salvato ? 'Notifiche attivate.' : 'Notifiche disattivate.',
+ );
+ } catch (e) {
+ if (!mounted) return;
+ setState(() => _pushEnabled = precedente);
+ _snack('Errore nel salvataggio: $e');
+ } finally {
+ if (mounted) setState(() => _savingPush = false);
+ }
}
@override
@@ -252,6 +295,8 @@ class _AccountPageState extends State {
const SizedBox(height: 20),
_profileCard(),
const SizedBox(height: 16),
+ _notificationsCard(),
+ const SizedBox(height: 16),
_passwordCard(),
const SizedBox(height: 8),
],
@@ -409,6 +454,47 @@ class _AccountPageState extends State {
);
}
+ Widget _notificationsCard() {
+ if (_pushEnabled == null) {
+ return _card(
+ title: 'Notifiche',
+ children: const [
+ Row(
+ children: [
+ SizedBox(
+ width: 18,
+ height: 18,
+ child: CircularProgressIndicator(strokeWidth: 2),
+ ),
+ SizedBox(width: 12),
+ Expanded(child: Text('Carico le impostazioni...')),
+ ],
+ ),
+ ],
+ );
+ }
+
+ return _card(
+ title: 'Notifiche',
+ children: [
+ SwitchListTile(
+ contentPadding: EdgeInsets.zero,
+ activeThumbColor: kGreen,
+ value: _pushEnabled!,
+ onChanged: _savingPush ? null : _togglePush,
+ title: const Text(
+ 'Notifiche push',
+ style: TextStyle(fontWeight: FontWeight.w700),
+ ),
+ subtitle: const Text(
+ 'Promemoria delle lezioni del giorno, scadenza abbonamento e '
+ 'certificato medico.',
+ ),
+ ),
+ ],
+ );
+ }
+
Widget _passwordCard() {
return _card(
title: 'Cambia password',
diff --git a/lib/screens/login_page.dart b/lib/screens/login_page.dart
index c9aa2a3..c7524c8 100644
--- a/lib/screens/login_page.dart
+++ b/lib/screens/login_page.dart
@@ -1,4 +1,7 @@
+import 'dart:async';
+
import 'package:flutter/material.dart';
+import '../services/push_service.dart';
import '../services/vanguard_api.dart';
import '../models/school.dart';
import 'home_page.dart';
@@ -47,6 +50,8 @@ class _LoginPageState extends State {
password: passwordController.text.trim(),
);
+ unawaited(PushService.registerAfterLogin(token));
+
if (!mounted) return;
// Bypass SelectSchoolPage: scuola unica, si va dritti alla Home.
diff --git a/lib/services/push_api.dart b/lib/services/push_api.dart
new file mode 100644
index 0000000..d8d8564
--- /dev/null
+++ b/lib/services/push_api.dart
@@ -0,0 +1,92 @@
+import 'dart:convert';
+import 'package:http/http.dart' as http;
+
+import '../config/api_config.dart';
+
+class PushApi {
+ static Map _headers(String token) => {
+ 'Accept': 'application/json',
+ 'Content-Type': 'application/json',
+ 'Authorization': 'Bearer $token',
+ };
+
+ static Future registerDevice({
+ required String token,
+ required String deviceToken,
+ required String platform,
+ String? appVersion,
+ }) async {
+ final uri = Uri.parse('${ApiConfig.laravelApiBase}/register_device.php');
+
+ final res = await http.post(
+ uri,
+ headers: _headers(token),
+ body: jsonEncode({
+ 'device_token': deviceToken,
+ 'platform': platform,
+ if (appVersion != null) 'app_version': appVersion,
+ }),
+ );
+
+ final data = _decode(res.body, res.statusCode);
+
+ return (data['push_enabled'] ?? 'Y').toString();
+ }
+
+ static Future unregisterDevice({
+ required String token,
+ required String deviceToken,
+ }) async {
+ final uri = Uri.parse('${ApiConfig.laravelApiBase}/register_device.php');
+
+ final res = await http.delete(
+ uri,
+ headers: _headers(token),
+ body: jsonEncode({'device_token': deviceToken}),
+ );
+
+ _decode(res.body, res.statusCode);
+ }
+
+ static Future fetchPreference({required String token}) async {
+ final uri = Uri.parse('${ApiConfig.laravelApiBase}/push_preference.php');
+
+ final res = await http.get(uri, headers: _headers(token));
+ final data = _decode(res.body, res.statusCode);
+
+ return (data['push_enabled'] ?? 'Y').toString().toUpperCase() == 'Y';
+ }
+
+ static Future updatePreference({
+ required String token,
+ required bool enabled,
+ }) async {
+ final uri = Uri.parse('${ApiConfig.laravelApiBase}/push_preference.php');
+
+ final res = await http.post(
+ uri,
+ headers: _headers(token),
+ body: jsonEncode({'enabled': enabled}),
+ );
+
+ final data = _decode(res.body, res.statusCode);
+
+ return (data['push_enabled'] ?? 'Y').toString().toUpperCase() == 'Y';
+ }
+
+ static Map _decode(String body, int statusCode) {
+ Map data;
+
+ try {
+ data = jsonDecode(body) as Map;
+ } catch (_) {
+ throw Exception('Risposta non valida ($statusCode): $body');
+ }
+
+ if (statusCode != 200 || data['success'] != true) {
+ throw Exception(data['error'] ?? data['message'] ?? 'Errore notifiche');
+ }
+
+ return data;
+ }
+}
diff --git a/lib/services/push_service.dart b/lib/services/push_service.dart
new file mode 100644
index 0000000..f84b3f6
--- /dev/null
+++ b/lib/services/push_service.dart
@@ -0,0 +1,162 @@
+import 'dart:io' show Platform;
+
+import 'package:firebase_core/firebase_core.dart';
+import 'package:firebase_messaging/firebase_messaging.dart';
+import 'package:flutter/foundation.dart';
+import 'package:flutter_local_notifications/flutter_local_notifications.dart';
+
+import 'push_api.dart';
+
+class PushService {
+ static String? _deviceToken;
+
+ static bool _firebaseReady = false;
+
+ static final FlutterLocalNotificationsPlugin _locali =
+ FlutterLocalNotificationsPlugin();
+
+ static const AndroidNotificationChannel _canale = AndroidNotificationChannel(
+ 'yogibook_notifiche',
+ 'Notifiche YogiBook',
+ description: 'Promemoria delle lezioni, scadenze abbonamento e certificato',
+ importance: Importance.high,
+ );
+
+ static Future initFirebase() async {
+ if (_firebaseReady) return;
+
+ try {
+ await Firebase.initializeApp();
+ FirebaseMessaging.onBackgroundMessage(_backgroundHandler);
+ await _preparaNotificheLocali();
+ _firebaseReady = true;
+ } catch (e) {
+ debugPrint('Firebase non inizializzato: $e');
+ }
+ }
+
+ static Future _preparaNotificheLocali() async {
+ const impostazioni = InitializationSettings(
+ android: AndroidInitializationSettings('@mipmap/ic_launcher'),
+ iOS: DarwinInitializationSettings(
+ requestAlertPermission: false,
+ requestBadgePermission: false,
+ requestSoundPermission: false,
+ ),
+ );
+
+ await _locali.initialize(settings: impostazioni);
+
+ await _locali
+ .resolvePlatformSpecificImplementation<
+ AndroidFlutterLocalNotificationsPlugin
+ >()
+ ?.createNotificationChannel(_canale);
+
+ FirebaseMessaging.onMessage.listen(_mostraInPrimoPiano);
+ }
+
+ static Future _mostraInPrimoPiano(RemoteMessage messaggio) async {
+ if (kIsWeb || !Platform.isAndroid) return;
+
+ final notifica = messaggio.notification;
+ if (notifica == null) return;
+
+ await _locali.show(
+ id: notifica.hashCode,
+ title: notifica.title,
+ body: notifica.body,
+ notificationDetails: NotificationDetails(
+ android: AndroidNotificationDetails(
+ _canale.id,
+ _canale.name,
+ channelDescription: _canale.description,
+ importance: Importance.high,
+ priority: Priority.high,
+ icon: '@mipmap/ic_launcher',
+ ),
+ ),
+ );
+ }
+
+ static Future registerAfterLogin(String authToken) async {
+ if (!_firebaseReady) return;
+
+ try {
+ final messaging = FirebaseMessaging.instance;
+
+ final settings = await messaging.requestPermission();
+
+ if (settings.authorizationStatus == AuthorizationStatus.denied) {
+ debugPrint('Permesso notifiche negato dall\'utente.');
+ return;
+ }
+
+ final deviceToken = await messaging.getToken();
+ if (deviceToken == null) return;
+
+ _deviceToken = deviceToken;
+
+ await PushApi.registerDevice(
+ token: authToken,
+ deviceToken: deviceToken,
+ platform: _platformName(),
+ );
+
+ messaging.onTokenRefresh.listen((nuovo) async {
+ _deviceToken = nuovo;
+ try {
+ await PushApi.registerDevice(
+ token: authToken,
+ deviceToken: nuovo,
+ platform: _platformName(),
+ );
+ } catch (e) {
+ debugPrint('Aggiornamento token fallito: $e');
+ }
+ });
+ } catch (e) {
+ debugPrint('Registrazione push fallita: $e');
+ }
+ }
+
+ static Future unregisterOnLogout(String authToken) async {
+ final deviceToken = _deviceToken;
+ if (deviceToken == null) return;
+
+ try {
+ await PushApi.unregisterDevice(
+ token: authToken,
+ deviceToken: deviceToken,
+ );
+ _deviceToken = null;
+ } catch (e) {
+ debugPrint('Disattivazione dispositivo fallita: $e');
+ }
+ }
+
+ static Future enableForegroundNotifications() async {
+ if (!_firebaseReady) return;
+
+ try {
+ await FirebaseMessaging.instance
+ .setForegroundNotificationPresentationOptions(
+ alert: true,
+ badge: true,
+ sound: true,
+ );
+ } catch (e) {
+ debugPrint('Impostazione notifiche in foreground fallita: $e');
+ }
+ }
+
+ static String _platformName() {
+ if (kIsWeb) return 'web';
+ return Platform.isIOS ? 'ios' : 'android';
+ }
+}
+
+@pragma('vm:entry-point')
+Future _backgroundHandler(RemoteMessage message) async {
+ debugPrint('Notifica in background: ${message.messageId}');
+}
diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift
index 793a31b..ec7651b 100644
--- a/macos/Flutter/GeneratedPluginRegistrant.swift
+++ b/macos/Flutter/GeneratedPluginRegistrant.swift
@@ -8,6 +8,9 @@ import Foundation
import audioplayers_darwin
import file_picker
import file_selector_macos
+import firebase_core
+import firebase_messaging
+import flutter_local_notifications
import google_sign_in_ios
import path_provider_foundation
import shared_preferences_foundation
@@ -17,6 +20,9 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
+ FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
+ FLTFirebaseMessagingPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseMessagingPlugin"))
+ FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin"))
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
diff --git a/pubspec.lock b/pubspec.lock
index 2f36b05..097e505 100644
--- a/pubspec.lock
+++ b/pubspec.lock
@@ -1,6 +1,22 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
+ _flutterfire_internals:
+ dependency: transitive
+ description:
+ name: _flutterfire_internals
+ sha256: "6727cf2ced9b104abca9daa278380be2eca2b98ce33d4b46f11708e387dc6b4d"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.3.76"
+ args:
+ dependency: transitive
+ description:
+ name: args
+ sha256: d0481093c50b1da8910eb0bb301626d4d8eb7284aa739614d2b394ee09e3ea04
+ url: "https://pub.dev"
+ source: hosted
+ version: "2.7.0"
async:
dependency: transitive
description:
@@ -121,6 +137,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.0.8"
+ dbus:
+ dependency: transitive
+ description:
+ name: dbus
+ sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.7.13"
fake_async:
dependency: transitive
description:
@@ -185,6 +209,54 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.9.3+5"
+ firebase_core:
+ dependency: "direct main"
+ description:
+ name: firebase_core
+ sha256: "9478ca6700c02d315c6aba37e206e612317f98bb4f335bb1cbd6e0ce67dcf764"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.13.0"
+ firebase_core_platform_interface:
+ dependency: transitive
+ description:
+ name: firebase_core_platform_interface
+ sha256: e28f9afdcb5b0f0a8ea74ea3b322f5a7592c81cb45dd9d189913bdae08a2089a
+ url: "https://pub.dev"
+ source: hosted
+ version: "8.1.0"
+ firebase_core_web:
+ dependency: transitive
+ description:
+ name: firebase_core_web
+ sha256: f471a288b0101a45567548322ac4a5ad31e3ecbf87a576dcc0e424e6a11e04ea
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.10.0"
+ firebase_messaging:
+ dependency: "direct main"
+ description:
+ name: firebase_messaging
+ sha256: "55cf1000dd72d229ebff3ce6a399bd35705a7675275478b31308afd90ac85751"
+ url: "https://pub.dev"
+ source: hosted
+ version: "16.5.0"
+ firebase_messaging_platform_interface:
+ dependency: transitive
+ description:
+ name: firebase_messaging_platform_interface
+ sha256: e5fbb62bd23a27c983825277877ac91fdfdb6bfb31c6e5399700963ae586e339
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.9.3"
+ firebase_messaging_web:
+ dependency: transitive
+ description:
+ name: firebase_messaging_web
+ sha256: "663d320e90a41fe57d651ec79df2bc831bf29e051bc0c2cd41faf3106eb412d9"
+ url: "https://pub.dev"
+ source: hosted
+ version: "4.2.4"
fixnum:
dependency: transitive
description:
@@ -206,6 +278,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "6.0.0"
+ flutter_local_notifications:
+ dependency: "direct main"
+ description:
+ name: flutter_local_notifications
+ sha256: "1447ba911c60f2ba3f25dae1af151ec187162566b0f57e37771bf0b400f013ad"
+ url: "https://pub.dev"
+ source: hosted
+ version: "22.3.0"
+ flutter_local_notifications_linux:
+ dependency: transitive
+ description:
+ name: flutter_local_notifications_linux
+ sha256: "9ca97e63776f29ab1b955725c09999fc2c150523269db150c39274f2a43c5a8b"
+ url: "https://pub.dev"
+ source: hosted
+ version: "8.0.1"
+ flutter_local_notifications_platform_interface:
+ dependency: transitive
+ description:
+ name: flutter_local_notifications_platform_interface
+ sha256: "43c3761d916c9bd3d5c7ebbc44d82f4990329840c0c5d62ad5260cc1b5d399bd"
+ url: "https://pub.dev"
+ source: hosted
+ version: "12.2.0"
+ flutter_local_notifications_web:
+ dependency: transitive
+ description:
+ name: flutter_local_notifications_web
+ sha256: "516afaf97a2d1e67a036c6617321b00d205d72f7a67b6eccf936cd565f985878"
+ url: "https://pub.dev"
+ source: hosted
+ version: "1.0.0"
+ flutter_local_notifications_windows:
+ dependency: transitive
+ description:
+ name: flutter_local_notifications_windows
+ sha256: "6f43bdd03b171b7a90f22647506fea33e2bb12294b7c7c7a3d690e960a382945"
+ url: "https://pub.dev"
+ source: hosted
+ version: "3.1.1"
flutter_localizations:
dependency: "direct main"
description: flutter
@@ -493,6 +605,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "2.3.0"
+ petitparser:
+ dependency: transitive
+ description:
+ name: petitparser
+ sha256: "91bd59303e9f769f108f8df05e371341b15d59e995e6806aefab827b58336675"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.2"
platform:
dependency: transitive
description:
@@ -630,10 +750,18 @@ packages:
dependency: transitive
description:
name: test_api
- sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636"
+ sha256: "2a122cbe059f8b610d3a5415f42e255b6c17b1f21eee1d960f31080237fb4f11"
url: "https://pub.dev"
source: hosted
- version: "0.7.9"
+ version: "0.7.12"
+ timezone:
+ dependency: transitive
+ description:
+ name: timezone
+ sha256: "981d1020d6ef8fe1e7b3de5054e5b25579ae7c403d7734adc508ffc47668e9cb"
+ url: "https://pub.dev"
+ source: hosted
+ version: "0.11.1"
typed_data:
dependency: transitive
description:
@@ -718,10 +846,10 @@ packages:
dependency: transitive
description:
name: vector_math
- sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
+ sha256: f36f9f3be64c6198714492bb455c11056e33e2f85d9a0b676a48301e44fdcf47
url: "https://pub.dev"
source: hosted
- version: "2.2.0"
+ version: "2.4.2"
vm_service:
dependency: transitive
description:
@@ -754,6 +882,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
+ xml:
+ dependency: transitive
+ description:
+ name: xml
+ sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
+ url: "https://pub.dev"
+ source: hosted
+ version: "7.0.1"
sdks:
- dart: ">=3.10.4 <4.0.0"
- flutter: ">=3.35.0"
+ dart: ">=3.11.0 <4.0.0"
+ flutter: ">=3.38.1"
diff --git a/pubspec.yaml b/pubspec.yaml
index e746574..4f62b50 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -44,6 +44,9 @@ dependencies:
mime: ^2.0.0
provider: ^6.1.5+1
shared_preferences: ^2.2.3
+ firebase_core: ^4.13.0
+ firebase_messaging: ^16.5.0
+ flutter_local_notifications: ^22.3.0
dev_dependencies:
flutter_test:
diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc
index 6e77110..cb386cc 100644
--- a/windows/flutter/generated_plugin_registrant.cc
+++ b/windows/flutter/generated_plugin_registrant.cc
@@ -8,6 +8,7 @@
#include
#include
+#include
#include
void RegisterPlugins(flutter::PluginRegistry* registry) {
@@ -15,6 +16,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("AudioplayersWindowsPlugin"));
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
+ FirebaseCorePluginCApiRegisterWithRegistrar(
+ registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
}
diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake
index 804cebd..8035f8b 100644
--- a/windows/flutter/generated_plugins.cmake
+++ b/windows/flutter/generated_plugins.cmake
@@ -5,10 +5,12 @@
list(APPEND FLUTTER_PLUGIN_LIST
audioplayers_windows
file_selector_windows
+ firebase_core
url_launcher_windows
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST
+ flutter_local_notifications_windows
)
set(PLUGIN_BUNDLED_LIBRARIES)