🐛 Trying to fix windows notification issue
This commit is contained in:
@@ -2,7 +2,6 @@ import 'dart:async';
|
|||||||
|
|
||||||
import 'package:easy_localization/easy_localization.dart';
|
import 'package:easy_localization/easy_localization.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
|
||||||
import 'package:flutter_hooks/flutter_hooks.dart';
|
import 'package:flutter_hooks/flutter_hooks.dart';
|
||||||
import 'package:gap/gap.dart';
|
import 'package:gap/gap.dart';
|
||||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
import 'package:hooks_riverpod/hooks_riverpod.dart';
|
||||||
|
@@ -1,230 +1,47 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
import 'dart:developer';
|
|
||||||
import 'dart:io';
|
import 'dart:io';
|
||||||
|
|
||||||
import 'package:dio/dio.dart';
|
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/material.dart';
|
||||||
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.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 =
|
// Conditional imports based on platform
|
||||||
FlutterLocalNotificationsPlugin();
|
import 'notify.windows.dart' as windows_notify;
|
||||||
|
import 'notify.universal.dart' as universal_notify;
|
||||||
AppLifecycleState _appLifecycleState = AppLifecycleState.resumed;
|
|
||||||
|
|
||||||
void _onAppLifecycleChanged(AppLifecycleState state) {
|
|
||||||
_appLifecycleState = state;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// Platform-specific delegation
|
||||||
Future<void> initializeLocalNotifications() async {
|
Future<void> initializeLocalNotifications() async {
|
||||||
const AndroidInitializationSettings initializationSettingsAndroid =
|
if (Platform.isWindows) {
|
||||||
AndroidInitializationSettings('@mipmap/ic_launcher');
|
return windows_notify.initializeLocalNotifications();
|
||||||
|
|
||||||
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 {
|
} else {
|
||||||
// External URLs
|
return universal_notify.initializeLocalNotifications();
|
||||||
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<WebSocketPacket> setupNotificationListener(
|
StreamSubscription setupNotificationListener(
|
||||||
BuildContext context,
|
BuildContext context,
|
||||||
WidgetRef ref,
|
WidgetRef ref,
|
||||||
) {
|
) {
|
||||||
final ws = ref.watch(websocketProvider);
|
if (Platform.isWindows) {
|
||||||
return ws.dataStream.listen((pkt) async {
|
return windows_notify.setupNotificationListener(context, ref);
|
||||||
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 {
|
} else {
|
||||||
// External URLs
|
return universal_notify.setupNotificationListener(context, ref);
|
||||||
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}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> subscribePushNotification(
|
Future<void> subscribePushNotification(
|
||||||
Dio apiClient, {
|
Dio apiClient, {
|
||||||
bool detailedErrors = false,
|
bool detailedErrors = false,
|
||||||
}) async {
|
}) async {
|
||||||
if (!kIsWeb && Platform.isLinux) {
|
if (Platform.isWindows) {
|
||||||
return;
|
return windows_notify.subscribePushNotification(
|
||||||
}
|
|
||||||
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,
|
apiClient,
|
||||||
deviceToken,
|
detailedErrors: detailedErrors,
|
||||||
!kIsWeb && (Platform.isIOS || Platform.isMacOS) ? 0 : 1,
|
);
|
||||||
|
} else {
|
||||||
|
return universal_notify.subscribePushNotification(
|
||||||
|
apiClient,
|
||||||
|
detailedErrors: detailedErrors,
|
||||||
);
|
);
|
||||||
} else if (detailedErrors) {
|
|
||||||
throw Exception("Failed to get device token for push notifications.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _putTokenToRemote(
|
|
||||||
Dio apiClient,
|
|
||||||
String token,
|
|
||||||
int provider,
|
|
||||||
) async {
|
|
||||||
await apiClient.put(
|
|
||||||
"/pusher/notifications/subscription",
|
|
||||||
data: {"provider": provider, "device_token": token},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
232
lib/services/notify.universal.dart
Normal file
232
lib/services/notify.universal.dart
Normal file
@@ -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<void> 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<WebSocketPacket> 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<void> 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<void> _putTokenToRemote(
|
||||||
|
Dio apiClient,
|
||||||
|
String token,
|
||||||
|
int provider,
|
||||||
|
) async {
|
||||||
|
await apiClient.put(
|
||||||
|
"/pusher/notifications/subscription",
|
||||||
|
data: {"provider": provider, "device_token": token},
|
||||||
|
);
|
||||||
|
}
|
176
lib/services/notify.windows.dart
Normal file
176
lib/services/notify.windows.dart
Normal file
@@ -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<void> 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<WebSocketPacket> 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<void> 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<void> _putTokenToRemote(
|
||||||
|
Dio apiClient,
|
||||||
|
String token,
|
||||||
|
int provider,
|
||||||
|
) async {
|
||||||
|
await apiClient.put(
|
||||||
|
"/pusher/notifications/subscription",
|
||||||
|
data: {"provider": provider, "device_token": token},
|
||||||
|
);
|
||||||
|
}
|
26
pubspec.lock
26
pubspec.lock
@@ -1281,10 +1281,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e"
|
sha256: a45bef33deb24839a51fb85a4d9e504ead2b1ad1c4779d02d09bf6a8857cdd52
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.13+1"
|
version: "0.8.13+2"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -1449,10 +1449,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: local_auth_android
|
name: local_auth_android
|
||||||
sha256: "48924f4a8b3cc45994ad5993e2e232d3b00788a305c1bf1c7db32cef281ce9a3"
|
sha256: "1ee0e63fb8b5c6fa286796b5fb1570d256857c2f4a262127e728b36b80a570cf"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.52"
|
version: "1.0.53"
|
||||||
local_auth_darwin:
|
local_auth_darwin:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2576,10 +2576,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: "69ee86740f2847b9a4ba6cffa74ed12ce500bbe2b07f3dc1e643439da60637b7"
|
sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.18"
|
version: "6.3.20"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2760,10 +2760,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: webrtc_interface
|
name: webrtc_interface
|
||||||
sha256: "86fe3afc81a08481dfb25cf14a5a94e27062ecef25544783f352c914e0bbc1ca"
|
sha256: "2e604a31703ad26781782fb14fa8a4ee621154ee2c513d2b9938e486fa695233"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.2+hotfix.2"
|
version: "1.3.0"
|
||||||
win32:
|
win32:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2780,6 +2780,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.0"
|
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:
|
xdg_directories:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -2806,4 +2814,4 @@ packages:
|
|||||||
version: "3.1.3"
|
version: "3.1.3"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.9.0 <4.0.0"
|
dart: ">=3.9.0 <4.0.0"
|
||||||
flutter: ">=3.32.0"
|
flutter: ">=3.35.0"
|
||||||
|
@@ -76,7 +76,7 @@ dependencies:
|
|||||||
file_picker: ^10.3.2
|
file_picker: ^10.3.2
|
||||||
riverpod_annotation: ^2.6.1
|
riverpod_annotation: ^2.6.1
|
||||||
image_picker_platform_interface: ^2.11.0
|
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
|
super_context_menu: ^0.9.1
|
||||||
modal_bottom_sheet: ^3.0.0
|
modal_bottom_sheet: ^3.0.0
|
||||||
firebase_messaging: ^16.0.1
|
firebase_messaging: ^16.0.1
|
||||||
@@ -147,6 +147,7 @@ dependencies:
|
|||||||
slide_countdown: ^2.0.2
|
slide_countdown: ^2.0.2
|
||||||
shelf: ^1.4.2
|
shelf: ^1.4.2
|
||||||
shelf_web_socket: ^3.0.0
|
shelf_web_socket: ^3.0.0
|
||||||
|
windows_notification: ^1.3.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
@@ -31,6 +31,7 @@
|
|||||||
#include <tray_manager/tray_manager_plugin.h>
|
#include <tray_manager/tray_manager_plugin.h>
|
||||||
#include <url_launcher_windows/url_launcher_windows.h>
|
#include <url_launcher_windows/url_launcher_windows.h>
|
||||||
#include <volume_controller/volume_controller_plugin_c_api.h>
|
#include <volume_controller/volume_controller_plugin_c_api.h>
|
||||||
|
#include <windows_notification/windows_notification_plugin_c_api.h>
|
||||||
|
|
||||||
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
void RegisterPlugins(flutter::PluginRegistry* registry) {
|
||||||
BitsdojoWindowPluginRegisterWithRegistrar(
|
BitsdojoWindowPluginRegisterWithRegistrar(
|
||||||
@@ -83,4 +84,6 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
|
|||||||
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
registry->GetRegistrarForPlugin("UrlLauncherWindows"));
|
||||||
VolumeControllerPluginCApiRegisterWithRegistrar(
|
VolumeControllerPluginCApiRegisterWithRegistrar(
|
||||||
registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
|
registry->GetRegistrarForPlugin("VolumeControllerPluginCApi"));
|
||||||
|
WindowsNotificationPluginCApiRegisterWithRegistrar(
|
||||||
|
registry->GetRegistrarForPlugin("WindowsNotificationPluginCApi"));
|
||||||
}
|
}
|
||||||
|
@@ -28,6 +28,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
|
|||||||
tray_manager
|
tray_manager
|
||||||
url_launcher_windows
|
url_launcher_windows
|
||||||
volume_controller
|
volume_controller
|
||||||
|
windows_notification
|
||||||
)
|
)
|
||||||
|
|
||||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||||
|
Reference in New Issue
Block a user