diff --git a/lib/screens/posts/compose_article.dart b/lib/screens/posts/compose_article.dart index 00a9bc09..7384c1a8 100644 --- a/lib/screens/posts/compose_article.dart +++ b/lib/screens/posts/compose_article.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:gap/gap.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; diff --git a/lib/services/notify.dart b/lib/services/notify.dart index dbb3db58..ad594662 100644 --- a/lib/services/notify.dart +++ b/lib/services/notify.dart @@ -1,230 +1,47 @@ import 'dart:async'; -import 'dart:developer'; import 'dart:io'; import 'package:dio/dio.dart'; -import 'package:firebase_messaging/firebase_messaging.dart'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_local_notifications/flutter_local_notifications.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:go_router/go_router.dart'; -import 'package:island/main.dart'; -import 'package:island/route.dart'; -import 'package:island/models/account.dart'; -import 'package:island/pods/websocket.dart'; -import 'package:island/widgets/app_notification.dart'; -import 'package:top_snackbar_flutter/top_snack_bar.dart'; -import 'package:url_launcher/url_launcher_string.dart'; -final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = - FlutterLocalNotificationsPlugin(); - -AppLifecycleState _appLifecycleState = AppLifecycleState.resumed; - -void _onAppLifecycleChanged(AppLifecycleState state) { - _appLifecycleState = state; -} +// Conditional imports based on platform +import 'notify.windows.dart' as windows_notify; +import 'notify.universal.dart' as universal_notify; +// Platform-specific delegation Future initializeLocalNotifications() async { - const AndroidInitializationSettings initializationSettingsAndroid = - AndroidInitializationSettings('@mipmap/ic_launcher'); - - const DarwinInitializationSettings initializationSettingsIOS = - DarwinInitializationSettings(); - - const DarwinInitializationSettings initializationSettingsMacOS = - DarwinInitializationSettings(); - - const LinuxInitializationSettings initializationSettingsLinux = - LinuxInitializationSettings(defaultActionName: 'Open notification'); - - const WindowsInitializationSettings initializationSettingsWindows = - WindowsInitializationSettings( - appName: 'Island', - appUserModelId: 'dev.solsynth.solian', - guid: 'dev.solsynth.solian', - ); - - const InitializationSettings initializationSettings = InitializationSettings( - android: initializationSettingsAndroid, - iOS: initializationSettingsIOS, - macOS: initializationSettingsMacOS, - linux: initializationSettingsLinux, - windows: initializationSettingsWindows, - ); - - await flutterLocalNotificationsPlugin.initialize( - initializationSettings, - onDidReceiveNotificationResponse: (NotificationResponse response) async { - final payload = response.payload; - if (payload != null) { - if (payload.startsWith('/')) { - // In-app routes - rootNavigatorKey.currentContext?.push(payload); - } else { - // External URLs - launchUrlString(payload); - } - } - }, - ); - - WidgetsBinding.instance.addObserver( - LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged), - ); -} - -class LifecycleEventHandler extends WidgetsBindingObserver { - final void Function(AppLifecycleState) onAppLifecycleChanged; - - LifecycleEventHandler({required this.onAppLifecycleChanged}); - - @override - void didChangeAppLifecycleState(AppLifecycleState state) { - onAppLifecycleChanged(state); + if (Platform.isWindows) { + return windows_notify.initializeLocalNotifications(); + } else { + return universal_notify.initializeLocalNotifications(); } } -StreamSubscription setupNotificationListener( +StreamSubscription setupNotificationListener( BuildContext context, WidgetRef ref, ) { - final ws = ref.watch(websocketProvider); - return ws.dataStream.listen((pkt) async { - if (pkt.type == "notifications.new") { - final notification = SnNotification.fromJson(pkt.data!); - if (_appLifecycleState == AppLifecycleState.resumed) { - // App is focused, show in-app notification - log( - '[Notification] Showing in-app notification: ${notification.title}', - ); - showTopSnackBar( - globalOverlay.currentState!, - Center( - child: ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 480), - child: NotificationCard(notification: notification), - ), - ), - onTap: () { - if (notification.meta['action_uri'] != null) { - var uri = notification.meta['action_uri'] as String; - if (uri.startsWith('/')) { - // In-app routes - rootNavigatorKey.currentContext?.push( - notification.meta['action_uri'], - ); - } else { - // External URLs - launchUrlString(uri); - } - } - }, - onDismissed: () {}, - dismissType: DismissType.onSwipe, - displayDuration: const Duration(seconds: 5), - snackBarPosition: SnackBarPosition.top, - padding: EdgeInsets.only( - left: 16, - right: 16, - top: - (!kIsWeb && - (Platform.isMacOS || - Platform.isWindows || - Platform.isLinux)) - ? 28 - // ignore: use_build_context_synchronously - : MediaQuery.of(context).padding.top + 16, - bottom: 16, - ), - ); - } else { - // App is in background, show system notification (only on supported platforms) - if (!kIsWeb && !Platform.isIOS) { - log( - '[Notification] Showing system notification: ${notification.title}', - ); - const AndroidNotificationDetails androidNotificationDetails = - AndroidNotificationDetails( - 'channel_id', - 'channel_name', - channelDescription: 'channel_description', - importance: Importance.max, - priority: Priority.high, - ticker: 'ticker', - ); - const NotificationDetails notificationDetails = NotificationDetails( - android: androidNotificationDetails, - ); - await flutterLocalNotificationsPlugin.show( - 0, - notification.title, - notification.content, - notificationDetails, - payload: notification.meta['action_uri'] as String?, - ); - } else { - log( - '[Notification] Skipping system notification for unsupported platform: ${notification.title}', - ); - } - } - } - }); + if (Platform.isWindows) { + return windows_notify.setupNotificationListener(context, ref); + } else { + return universal_notify.setupNotificationListener(context, ref); + } } Future subscribePushNotification( Dio apiClient, { bool detailedErrors = false, }) async { - if (!kIsWeb && Platform.isLinux) { - return; - } - await FirebaseMessaging.instance.requestPermission( - alert: true, - badge: true, - sound: true, - ); - - String? deviceToken; - if (kIsWeb) { - deviceToken = await FirebaseMessaging.instance.getToken( - vapidKey: - "BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU", - ); - } else if (Platform.isAndroid) { - deviceToken = await FirebaseMessaging.instance.getToken(); - } else if (Platform.isIOS) { - deviceToken = await FirebaseMessaging.instance.getAPNSToken(); - } - - FirebaseMessaging.instance.onTokenRefresh - .listen((fcmToken) { - _putTokenToRemote(apiClient, fcmToken, 1); - }) - .onError((err) { - log("Failed to get firebase cloud messaging push token: $err"); - }); - - if (deviceToken != null) { - _putTokenToRemote( + if (Platform.isWindows) { + return windows_notify.subscribePushNotification( apiClient, - deviceToken, - !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1, + detailedErrors: detailedErrors, + ); + } else { + return universal_notify.subscribePushNotification( + apiClient, + detailedErrors: detailedErrors, ); - } else if (detailedErrors) { - throw Exception("Failed to get device token for push notifications."); } } - -Future _putTokenToRemote( - Dio apiClient, - String token, - int provider, -) async { - await apiClient.put( - "/pusher/notifications/subscription", - data: {"provider": provider, "device_token": token}, - ); -} diff --git a/lib/services/notify.universal.dart b/lib/services/notify.universal.dart new file mode 100644 index 00000000..5b87c978 --- /dev/null +++ b/lib/services/notify.universal.dart @@ -0,0 +1,232 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:dio/dio.dart'; +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:island/main.dart'; +import 'package:island/route.dart'; +import 'package:island/models/account.dart'; +import 'package:island/pods/websocket.dart'; +import 'package:island/widgets/app_notification.dart'; +import 'package:top_snackbar_flutter/top_snack_bar.dart'; +import 'package:url_launcher/url_launcher_string.dart'; + +final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + +AppLifecycleState _appLifecycleState = AppLifecycleState.resumed; + +void _onAppLifecycleChanged(AppLifecycleState state) { + _appLifecycleState = state; +} + +Future initializeLocalNotifications() async { + const AndroidInitializationSettings initializationSettingsAndroid = + AndroidInitializationSettings('@mipmap/ic_launcher'); + + const DarwinInitializationSettings initializationSettingsIOS = + DarwinInitializationSettings(); + + const DarwinInitializationSettings initializationSettingsMacOS = + DarwinInitializationSettings(); + + const LinuxInitializationSettings initializationSettingsLinux = + LinuxInitializationSettings(defaultActionName: 'Open notification'); + + const WindowsInitializationSettings initializationSettingsWindows = + WindowsInitializationSettings( + appName: 'Island', + appUserModelId: 'dev.solsynth.solian', + guid: 'dev.solsynth.solian', + ); + + const InitializationSettings initializationSettings = InitializationSettings( + android: initializationSettingsAndroid, + iOS: initializationSettingsIOS, + macOS: initializationSettingsMacOS, + linux: initializationSettingsLinux, + windows: initializationSettingsWindows, + ); + + await flutterLocalNotificationsPlugin.initialize( + initializationSettings, + onDidReceiveNotificationResponse: (NotificationResponse response) async { + final payload = response.payload; + if (payload != null) { + if (payload.startsWith('/')) { + // In-app routes + rootNavigatorKey.currentContext?.push(payload); + } else { + // External URLs + launchUrlString(payload); + } + } + }, + ); + + WidgetsBinding.instance.addObserver( + LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged), + ); +} + +class LifecycleEventHandler extends WidgetsBindingObserver { + final void Function(AppLifecycleState) onAppLifecycleChanged; + + LifecycleEventHandler({required this.onAppLifecycleChanged}); + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + onAppLifecycleChanged(state); + } +} + +StreamSubscription setupNotificationListener( + BuildContext context, + WidgetRef ref, +) { + final ws = ref.watch(websocketProvider); + return ws.dataStream.listen((pkt) async { + if (pkt.type == "notifications.new") { + final notification = SnNotification.fromJson(pkt.data!); + if (_appLifecycleState == AppLifecycleState.resumed) { + // App is focused, show in-app notification + log( + '[Notification] Showing in-app notification: ${notification.title}', + ); + showTopSnackBar( + globalOverlay.currentState!, + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 480), + child: NotificationCard(notification: notification), + ), + ), + onTap: () { + if (notification.meta['action_uri'] != null) { + var uri = notification.meta['action_uri'] as String; + if (uri.startsWith('/')) { + // In-app routes + rootNavigatorKey.currentContext?.push( + notification.meta['action_uri'], + ); + } else { + // External URLs + launchUrlString(uri); + } + } + }, + onDismissed: () {}, + dismissType: DismissType.onSwipe, + displayDuration: const Duration(seconds: 5), + snackBarPosition: SnackBarPosition.top, + padding: EdgeInsets.only( + left: 16, + right: 16, + top: + (!kIsWeb && + (Platform.isMacOS || + Platform.isWindows || + Platform.isLinux)) + ? 28 + // ignore: use_build_context_synchronously + : MediaQuery.of(context).padding.top + 16, + bottom: 16, + ), + ); + } else { + // App is in background, show system notification (only on supported platforms) + if (!kIsWeb && !Platform.isIOS) { + log( + '[Notification] Showing system notification: ${notification.title}', + ); + + // Use flutter_local_notifications for universal platforms + const AndroidNotificationDetails androidNotificationDetails = + AndroidNotificationDetails( + 'channel_id', + 'channel_name', + channelDescription: 'channel_description', + importance: Importance.max, + priority: Priority.high, + ticker: 'ticker', + ); + const NotificationDetails notificationDetails = NotificationDetails( + android: androidNotificationDetails, + ); + await flutterLocalNotificationsPlugin.show( + 0, + notification.title, + notification.content, + notificationDetails, + payload: notification.meta['action_uri'] as String?, + ); + } else { + log( + '[Notification] Skipping system notification for unsupported platform: ${notification.title}', + ); + } + } + } + }); +} + +Future subscribePushNotification( + Dio apiClient, { + bool detailedErrors = false, +}) async { + if (!kIsWeb && Platform.isLinux) { + return; + } + await FirebaseMessaging.instance.requestPermission( + alert: true, + badge: true, + sound: true, + ); + + String? deviceToken; + if (kIsWeb) { + deviceToken = await FirebaseMessaging.instance.getToken( + vapidKey: + "BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU", + ); + } else if (Platform.isAndroid) { + deviceToken = await FirebaseMessaging.instance.getToken(); + } else if (Platform.isIOS) { + deviceToken = await FirebaseMessaging.instance.getAPNSToken(); + } + + FirebaseMessaging.instance.onTokenRefresh + .listen((fcmToken) { + _putTokenToRemote(apiClient, fcmToken, 1); + }) + .onError((err) { + log("Failed to get firebase cloud messaging push token: $err"); + }); + + if (deviceToken != null) { + _putTokenToRemote( + apiClient, + deviceToken, + !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1, + ); + } else if (detailedErrors) { + throw Exception("Failed to get device token for push notifications."); + } +} + +Future _putTokenToRemote( + Dio apiClient, + String token, + int provider, +) async { + await apiClient.put( + "/pusher/notifications/subscription", + data: {"provider": provider, "device_token": token}, + ); +} diff --git a/lib/services/notify.windows.dart b/lib/services/notify.windows.dart new file mode 100644 index 00000000..c1033444 --- /dev/null +++ b/lib/services/notify.windows.dart @@ -0,0 +1,176 @@ +import 'dart:async'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:firebase_messaging/firebase_messaging.dart'; +import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:island/main.dart'; +import 'package:island/route.dart'; +import 'package:island/models/account.dart'; +import 'package:island/pods/websocket.dart'; +import 'package:island/widgets/app_notification.dart'; +import 'package:top_snackbar_flutter/top_snack_bar.dart'; +import 'package:url_launcher/url_launcher_string.dart'; +import 'package:windows_notification/windows_notification.dart' + as windows_notification; +import 'package:windows_notification/notification_message.dart'; + +import 'package:dio/dio.dart'; + +// Windows notification instance +windows_notification.WindowsNotification? windowsNotification; + +AppLifecycleState _appLifecycleState = AppLifecycleState.resumed; + +void _onAppLifecycleChanged(AppLifecycleState state) { + _appLifecycleState = state; +} + +Future initializeLocalNotifications() async { + // Initialize Windows notification for Windows platform + windowsNotification = windows_notification.WindowsNotification( + applicationId: 'dev.solsynth.solian', + ); + + WidgetsBinding.instance.addObserver( + LifecycleEventHandler(onAppLifecycleChanged: _onAppLifecycleChanged), + ); +} + +class LifecycleEventHandler extends WidgetsBindingObserver { + final void Function(AppLifecycleState) onAppLifecycleChanged; + + LifecycleEventHandler({required this.onAppLifecycleChanged}); + + @override + void didChangeAppLifecycleState(AppLifecycleState state) { + onAppLifecycleChanged(state); + } +} + +StreamSubscription setupNotificationListener( + BuildContext context, + WidgetRef ref, +) { + final ws = ref.watch(websocketProvider); + return ws.dataStream.listen((pkt) async { + if (pkt.type == "notifications.new") { + final notification = SnNotification.fromJson(pkt.data!); + if (_appLifecycleState == AppLifecycleState.resumed) { + // App is focused, show in-app notification + log( + '[Notification] Showing in-app notification: ${notification.title}', + ); + showTopSnackBar( + globalOverlay.currentState!, + Center( + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 480), + child: NotificationCard(notification: notification), + ), + ), + onTap: () { + if (notification.meta['action_uri'] != null) { + var uri = notification.meta['action_uri'] as String; + if (uri.startsWith('/')) { + // In-app routes + rootNavigatorKey.currentContext?.push( + notification.meta['action_uri'], + ); + } else { + // External URLs + launchUrlString(uri); + } + } + }, + onDismissed: () {}, + dismissType: DismissType.onSwipe, + displayDuration: const Duration(seconds: 5), + snackBarPosition: SnackBarPosition.top, + padding: EdgeInsets.only( + left: 16, + right: 16, + top: 28, // Windows specific padding + bottom: 16, + ), + ); + } else { + // App is in background, show Windows system notification + log( + '[Notification] Showing Windows system notification: ${notification.title}', + ); + + if (windowsNotification != null) { + // Use Windows notification for Windows platform + final notificationMessage = NotificationMessage.fromPluginTemplate( + DateTime.now().millisecondsSinceEpoch.toString(), // unique id + notification.title, + notification.content, + launch: notification.meta['action_uri'] as String?, + ); + await windowsNotification!.showNotificationPluginTemplate( + notificationMessage, + ); + } + } + } + }); +} + +Future subscribePushNotification( + Dio apiClient, { + bool detailedErrors = false, +}) async { + if (!kIsWeb && Platform.isLinux) { + return; + } + await FirebaseMessaging.instance.requestPermission( + alert: true, + badge: true, + sound: true, + ); + + String? deviceToken; + if (kIsWeb) { + deviceToken = await FirebaseMessaging.instance.getToken( + vapidKey: + "BFN2mkqyeI6oi4d2PAV4pfNyG3Jy0FBEblmmPrjmP0r5lHOPrxrcqLIWhM21R_cicF-j4Xhtr1kyDyDgJYRPLgU", + ); + } else if (Platform.isAndroid) { + deviceToken = await FirebaseMessaging.instance.getToken(); + } else if (Platform.isIOS) { + deviceToken = await FirebaseMessaging.instance.getAPNSToken(); + } + + FirebaseMessaging.instance.onTokenRefresh + .listen((fcmToken) { + _putTokenToRemote(apiClient, fcmToken, 1); + }) + .onError((err) { + log("Failed to get firebase cloud messaging push token: $err"); + }); + + if (deviceToken != null) { + _putTokenToRemote( + apiClient, + deviceToken, + !kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1, + ); + } else if (detailedErrors) { + throw Exception("Failed to get device token for push notifications."); + } +} + +Future _putTokenToRemote( + Dio apiClient, + String token, + int provider, +) async { + await apiClient.put( + "/pusher/notifications/subscription", + data: {"provider": provider, "device_token": token}, + ); +} diff --git a/pubspec.lock b/pubspec.lock index d6e4f94f..05aac587 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1281,10 +1281,10 @@ packages: dependency: "direct main" description: name: image_picker_android - sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e" + sha256: a45bef33deb24839a51fb85a4d9e504ead2b1ad1c4779d02d09bf6a8857cdd52 url: "https://pub.dev" source: hosted - version: "0.8.13+1" + version: "0.8.13+2" image_picker_for_web: dependency: transitive description: @@ -1449,10 +1449,10 @@ packages: dependency: transitive description: name: local_auth_android - sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3" + sha256: "1ee0e63fb8b5c6fa286796b5fb1570d256857c2f4a262127e728b36b80a570cf" url: "https://pub.dev" source: hosted - version: "1.0.52" + version: "1.0.53" local_auth_darwin: dependency: transitive description: @@ -2576,10 +2576,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7" + sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e" url: "https://pub.dev" source: hosted - version: "6.3.18" + version: "6.3.20" url_launcher_ios: dependency: transitive description: @@ -2760,10 +2760,10 @@ packages: dependency: transitive description: name: webrtc_interface - sha256: "86fe3afc81a08481dfb25cf14a5a94e27062ecef25544783f352c914e0bbc1ca" + sha256: "2e604a31703ad26781782fb14fa8a4ee621154ee2c513d2b9938e486fa695233" url: "https://pub.dev" source: hosted - version: "1.2.2+hotfix.2" + version: "1.3.0" win32: dependency: transitive description: @@ -2780,6 +2780,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + windows_notification: + dependency: "direct main" + description: + name: windows_notification + sha256: be3e650874615f315402c9b9f3656e29af156709c4b5cc272cb4ca0ab7ba94a8 + url: "https://pub.dev" + source: hosted + version: "1.3.0" xdg_directories: dependency: transitive description: @@ -2806,4 +2814,4 @@ packages: version: "3.1.3" sdks: dart: ">=3.9.0 <4.0.0" - flutter: ">=3.32.0" + flutter: ">=3.35.0" diff --git a/pubspec.yaml b/pubspec.yaml index d15ff6ad..e1472e3c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -76,7 +76,7 @@ dependencies: file_picker: ^10.3.2 riverpod_annotation: ^2.6.1 image_picker_platform_interface: ^2.11.0 - image_picker_android: ^0.8.13+1 + image_picker_android: ^0.8.13+2 super_context_menu: ^0.9.1 modal_bottom_sheet: ^3.0.0 firebase_messaging: ^16.0.1 @@ -147,6 +147,7 @@ dependencies: slide_countdown: ^2.0.2 shelf: ^1.4.2 shelf_web_socket: ^3.0.0 + windows_notification: ^1.3.0 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 5f486e7e..661c9404 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -31,6 +31,7 @@ #include #include #include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { BitsdojoWindowPluginRegisterWithRegistrar( @@ -83,4 +84,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("UrlLauncherWindows")); VolumeControllerPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("VolumeControllerPluginCApi")); + WindowsNotificationPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("WindowsNotificationPluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index e43e01bd..be1b3c07 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -28,6 +28,7 @@ list(APPEND FLUTTER_PLUGIN_LIST tray_manager url_launcher_windows volume_controller + windows_notification ) list(APPEND FLUTTER_FFI_PLUGIN_LIST