Compare commits
	
		
			6 Commits
		
	
	
		
			3.2.0+132
			...
			5363afa558
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						5363afa558
	
				 | 
					
					
						|||
| 
						
						
							
						
						f0d2737da8
	
				 | 
					
					
						|||
| 
						
						
							
						
						1f2f80aa3e
	
				 | 
					
					
						|||
| 
						
						
							
						
						240a872e65
	
				 | 
					
					
						|||
| c1ec6f0849 | |||
| ab42686d4d | 
@@ -79,6 +79,11 @@ class ActivityRpcServer {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  // Find available IPC socket path
 | 
					  // Find available IPC socket path
 | 
				
			||||||
  Future<String> _findAvailableIpcPath() async {
 | 
					  Future<String> _findAvailableIpcPath() async {
 | 
				
			||||||
 | 
					    if (Platform.isWindows) {
 | 
				
			||||||
 | 
					      // Use TCP sockets on Windows for IPC (simpler and more compatible)
 | 
				
			||||||
 | 
					      return _findAvailableTcpPort();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Build list of directories to try, with macOS-specific handling
 | 
					    // Build list of directories to try, with macOS-specific handling
 | 
				
			||||||
    final baseDirs = <String>[];
 | 
					    final baseDirs = <String>[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -145,6 +150,35 @@ class ActivityRpcServer {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // Find available TCP port for Windows IPC
 | 
				
			||||||
 | 
					  Future<String> _findAvailableTcpPort() async {
 | 
				
			||||||
 | 
					    // Use ports in the range 6473-6482 (different from WebSocket server range 6463-6472)
 | 
				
			||||||
 | 
					    for (int port = 6473; port <= 6482; port++) {
 | 
				
			||||||
 | 
					      try {
 | 
				
			||||||
 | 
					        final socket = await ServerSocket.bind(
 | 
				
			||||||
 | 
					          InternetAddress.loopbackIPv4,
 | 
				
			||||||
 | 
					          port,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        socket.close();
 | 
				
			||||||
 | 
					        developer.log(
 | 
				
			||||||
 | 
					          'IPC TCP socket will be created on port: $port',
 | 
				
			||||||
 | 
					          name: kRpcIpcLogPrefix,
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        return port.toString(); // Return as string to match existing interface
 | 
				
			||||||
 | 
					      } catch (e) {
 | 
				
			||||||
 | 
					        // Port not available, try next
 | 
				
			||||||
 | 
					        if (port == 6473) {
 | 
				
			||||||
 | 
					          developer.log(
 | 
				
			||||||
 | 
					            'IPC TCP port $port not available: $e',
 | 
				
			||||||
 | 
					            name: kRpcIpcLogPrefix,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        continue;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    throw Exception('No available IPC TCP ports found');
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Start the WebSocket server
 | 
					  // Start the WebSocket server
 | 
				
			||||||
  Future<void> start() async {
 | 
					  Future<void> start() async {
 | 
				
			||||||
    int port = portRange[0];
 | 
					    int port = portRange[0];
 | 
				
			||||||
@@ -197,12 +231,27 @@ class ActivityRpcServer {
 | 
				
			|||||||
    final shouldStartIpc = !Platform.isMacOS;
 | 
					    final shouldStartIpc = !Platform.isMacOS;
 | 
				
			||||||
    if (shouldStartIpc) {
 | 
					    if (shouldStartIpc) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
 | 
					        if (Platform.isWindows) {
 | 
				
			||||||
 | 
					          // Use TCP socket on Windows
 | 
				
			||||||
 | 
					          final ipcPortStr = await _findAvailableIpcPath();
 | 
				
			||||||
 | 
					          final ipcPort = int.parse(ipcPortStr);
 | 
				
			||||||
 | 
					          _ipcServer = await ServerSocket.bind(
 | 
				
			||||||
 | 
					            InternetAddress.loopbackIPv4,
 | 
				
			||||||
 | 
					            ipcPort,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          developer.log(
 | 
				
			||||||
 | 
					            'IPC listening on TCP port $ipcPort',
 | 
				
			||||||
 | 
					            name: kRpcIpcLogPrefix,
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          // Use Unix socket on other platforms
 | 
				
			||||||
          final ipcPath = await _findAvailableIpcPath();
 | 
					          final ipcPath = await _findAvailableIpcPath();
 | 
				
			||||||
          _ipcServer = await ServerSocket.bind(
 | 
					          _ipcServer = await ServerSocket.bind(
 | 
				
			||||||
            InternetAddress(ipcPath, type: InternetAddressType.unix),
 | 
					            InternetAddress(ipcPath, type: InternetAddressType.unix),
 | 
				
			||||||
            0,
 | 
					            0,
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
          developer.log('IPC listening at $ipcPath', name: kRpcIpcLogPrefix);
 | 
					          developer.log('IPC listening at $ipcPath', name: kRpcIpcLogPrefix);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        _ipcServer!.listen((Socket socket) {
 | 
					        _ipcServer!.listen((Socket socket) {
 | 
				
			||||||
          _onIpcConnection(socket);
 | 
					          _onIpcConnection(socket);
 | 
				
			||||||
@@ -213,7 +262,7 @@ class ActivityRpcServer {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      developer.log(
 | 
					      developer.log(
 | 
				
			||||||
        'IPC server disabled on macOS in production mode due to sandboxing',
 | 
					        'IPC server disabled on macOS due to sandboxing',
 | 
				
			||||||
        name: kRpcIpcLogPrefix,
 | 
					        name: kRpcIpcLogPrefix,
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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},
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										24
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										24
									
								
								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:
 | 
				
			||||||
@@ -2765,7 +2765,7 @@ packages:
 | 
				
			|||||||
    source: hosted
 | 
					    source: hosted
 | 
				
			||||||
    version: "1.3.0"
 | 
					    version: "1.3.0"
 | 
				
			||||||
  win32:
 | 
					  win32:
 | 
				
			||||||
    dependency: transitive
 | 
					    dependency: "direct main"
 | 
				
			||||||
    description:
 | 
					    description:
 | 
				
			||||||
      name: win32
 | 
					      name: win32
 | 
				
			||||||
      sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
 | 
					      sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
 | 
				
			||||||
@@ -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