Compare commits
	
		
			12 Commits
		
	
	
		
			3.2.0+132
			...
			488055955c
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						
						
							
						
						488055955c
	
				 | 
					
					
						|||
| 
						 | 
					313ebc64cc | ||
| 
						 | 
					1ed8b1d0c1 | ||
| 4af816d931 | |||
| 1c058a4323 | |||
| 461ed1fcda | |||
| 
						
						
							
						
						5363afa558
	
				 | 
					
					
						|||
| 
						
						
							
						
						f0d2737da8
	
				 | 
					
					
						|||
| 
						
						
							
						
						1f2f80aa3e
	
				 | 
					
					
						|||
| 
						
						
							
						
						240a872e65
	
				 | 
					
					
						|||
| c1ec6f0849 | |||
| ab42686d4d | 
@@ -181,7 +181,7 @@ class IslandApp extends HookConsumerWidget {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    useEffect(() {
 | 
			
		||||
      if (!kIsWeb && Platform.isLinux) {
 | 
			
		||||
      if (!kIsWeb && (Platform.isLinux || Platform.isWindows)) {
 | 
			
		||||
        return null;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,9 @@ import 'package:shelf/shelf.dart';
 | 
			
		||||
import 'package:shelf/shelf_io.dart' as shelf_io;
 | 
			
		||||
import 'package:shelf_web_socket/shelf_web_socket.dart';
 | 
			
		||||
import 'package:web_socket_channel/web_socket_channel.dart';
 | 
			
		||||
import 'package:path/path.dart' as path;
 | 
			
		||||
import 'ipc_server.dart';
 | 
			
		||||
import 'ipc_server.windows.dart';
 | 
			
		||||
import 'ipc_server.unix.dart';
 | 
			
		||||
 | 
			
		||||
const String kRpcLogPrefix = 'arRPC.websocket';
 | 
			
		||||
const String kRpcIpcLogPrefix = 'arRPC.ipc';
 | 
			
		||||
@@ -43,14 +45,14 @@ class IpcErrorCodes {
 | 
			
		||||
  static const int invalidEncoding = 4005;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Reference https://github.com/OpenAsar/arrpc/blob/main/src/transports/ipc.js
 | 
			
		||||
class ActivityRpcServer {
 | 
			
		||||
  static const List<int> portRange = [6463, 6472]; // Ports 6463–6472
 | 
			
		||||
  Map<String, Function>
 | 
			
		||||
  handlers; // {connection: (socket), message: (socket, data), close: (socket)}
 | 
			
		||||
  HttpServer? _httpServer;
 | 
			
		||||
  ServerSocket? _ipcServer;
 | 
			
		||||
  IpcServer? _ipcServer;
 | 
			
		||||
  final List<WebSocketChannel> _wsSockets = [];
 | 
			
		||||
  final List<_IpcSocketWrapper> _ipcSockets = [];
 | 
			
		||||
 | 
			
		||||
  ActivityRpcServer(this.handlers);
 | 
			
		||||
 | 
			
		||||
@@ -58,109 +60,20 @@ class ActivityRpcServer {
 | 
			
		||||
    handlers = newHandlers;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Encode IPC packet
 | 
			
		||||
  static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) {
 | 
			
		||||
    final jsonData = jsonEncode(data);
 | 
			
		||||
    final dataBytes = utf8.encode(jsonData);
 | 
			
		||||
    final dataSize = dataBytes.length;
 | 
			
		||||
 | 
			
		||||
    final buffer = ByteData(8 + dataSize);
 | 
			
		||||
    buffer.setInt32(0, type, Endian.little);
 | 
			
		||||
    buffer.setInt32(4, dataSize, Endian.little);
 | 
			
		||||
    buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes);
 | 
			
		||||
 | 
			
		||||
    return buffer.buffer.asUint8List();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<String> _getMacOsSystemTmpDir() async {
 | 
			
		||||
    final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']);
 | 
			
		||||
    return (result.stdout as String).trim();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Find available IPC socket path
 | 
			
		||||
  Future<String> _findAvailableIpcPath() async {
 | 
			
		||||
    // Build list of directories to try, with macOS-specific handling
 | 
			
		||||
    final baseDirs = <String>[];
 | 
			
		||||
 | 
			
		||||
    if (Platform.isMacOS) {
 | 
			
		||||
      try {
 | 
			
		||||
        final macTempDir = await _getMacOsSystemTmpDir();
 | 
			
		||||
        if (macTempDir.isNotEmpty) {
 | 
			
		||||
          baseDirs.add(macTempDir);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        developer.log(
 | 
			
		||||
          'Failed to get macOS system temp dir: $e',
 | 
			
		||||
          name: kRpcIpcLogPrefix,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add other standard directories
 | 
			
		||||
    final otherDirs = [
 | 
			
		||||
      Platform.environment['XDG_RUNTIME_DIR'], // User runtime directory
 | 
			
		||||
      Platform.environment['TMPDIR'], // App container temp (fallback)
 | 
			
		||||
      Platform.environment['TMP'],
 | 
			
		||||
      Platform.environment['TEMP'],
 | 
			
		||||
      '/tmp', // System temp directory - most compatible
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    baseDirs.addAll(
 | 
			
		||||
      otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast<String>(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    for (final baseDir in baseDirs) {
 | 
			
		||||
      for (int i = 0; i < 10; i++) {
 | 
			
		||||
        final socketPath = path.join(baseDir, '$kIpcBasePath-$i');
 | 
			
		||||
        try {
 | 
			
		||||
          final socket = await ServerSocket.bind(
 | 
			
		||||
            InternetAddress(socketPath, type: InternetAddressType.unix),
 | 
			
		||||
            0,
 | 
			
		||||
          );
 | 
			
		||||
          socket.close();
 | 
			
		||||
          // Clean up the test socket
 | 
			
		||||
          try {
 | 
			
		||||
            await File(socketPath).delete();
 | 
			
		||||
          } catch (_) {}
 | 
			
		||||
          developer.log(
 | 
			
		||||
            'IPC socket will be created at: $socketPath',
 | 
			
		||||
            name: kRpcIpcLogPrefix,
 | 
			
		||||
          );
 | 
			
		||||
          return socketPath;
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          // Path not available, try next
 | 
			
		||||
          if (i == 0) {
 | 
			
		||||
            // Log only for the first attempt per directory
 | 
			
		||||
            developer.log(
 | 
			
		||||
              'IPC path $socketPath not available: $e',
 | 
			
		||||
              name: kRpcIpcLogPrefix,
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    throw Exception(
 | 
			
		||||
      'No available IPC socket paths found in any temp directory',
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Start the WebSocket server
 | 
			
		||||
  // Start the server
 | 
			
		||||
  Future<void> start() async {
 | 
			
		||||
    int port = portRange[0];
 | 
			
		||||
    bool wsSuccess = false;
 | 
			
		||||
 | 
			
		||||
    // Start WebSocket server
 | 
			
		||||
    while (port <= portRange[1]) {
 | 
			
		||||
      developer.log('trying port $port', name: kRpcLogPrefix);
 | 
			
		||||
      developer.log('Trying port $port', name: kRpcLogPrefix);
 | 
			
		||||
      try {
 | 
			
		||||
        // Start HTTP server
 | 
			
		||||
        _httpServer = await HttpServer.bind(InternetAddress.loopbackIPv4, port);
 | 
			
		||||
        developer.log('listening on $port', name: kRpcLogPrefix);
 | 
			
		||||
        developer.log('Listening on $port', name: kRpcLogPrefix);
 | 
			
		||||
 | 
			
		||||
        // Handle WebSocket upgrades
 | 
			
		||||
        shelf_io.serveRequests(_httpServer!, (Request request) async {
 | 
			
		||||
          developer.log('new request', name: kRpcLogPrefix);
 | 
			
		||||
          developer.log('New request', name: kRpcLogPrefix);
 | 
			
		||||
          if (request.headers['upgrade']?.toLowerCase() == 'websocket') {
 | 
			
		||||
            final handler = webSocketHandler((WebSocketChannel channel, _) {
 | 
			
		||||
              _wsSockets.add(channel);
 | 
			
		||||
@@ -169,7 +82,7 @@ class ActivityRpcServer {
 | 
			
		||||
            return handler(request);
 | 
			
		||||
          }
 | 
			
		||||
          developer.log(
 | 
			
		||||
            'new request disposed due to not websocket',
 | 
			
		||||
            'New request disposed due to not websocket',
 | 
			
		||||
            name: kRpcLogPrefix,
 | 
			
		||||
          );
 | 
			
		||||
          return Response.notFound('Not a WebSocket request');
 | 
			
		||||
@@ -178,12 +91,12 @@ class ActivityRpcServer {
 | 
			
		||||
        break;
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        if (e is SocketException && e.osError?.errorCode == 98) {
 | 
			
		||||
          // EADDRINUSE
 | 
			
		||||
          developer.log('$port in use!', name: kRpcLogPrefix);
 | 
			
		||||
        } else {
 | 
			
		||||
          developer.log('http error: $e', name: kRpcLogPrefix);
 | 
			
		||||
          developer.log('HTTP error: $e', name: kRpcLogPrefix);
 | 
			
		||||
        }
 | 
			
		||||
        port++;
 | 
			
		||||
        await Future.delayed(Duration(milliseconds: 100)); // Add delay
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -193,27 +106,28 @@ class ActivityRpcServer {
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Start IPC server (skip on macOS due to sandboxing)
 | 
			
		||||
    final shouldStartIpc = !Platform.isMacOS;
 | 
			
		||||
    // Start IPC server
 | 
			
		||||
    final shouldStartIpc = !Platform.isMacOS && !kIsWeb;
 | 
			
		||||
    if (shouldStartIpc) {
 | 
			
		||||
      try {
 | 
			
		||||
        final ipcPath = await _findAvailableIpcPath();
 | 
			
		||||
        _ipcServer = await ServerSocket.bind(
 | 
			
		||||
          InternetAddress(ipcPath, type: InternetAddressType.unix),
 | 
			
		||||
          0,
 | 
			
		||||
        );
 | 
			
		||||
        developer.log('IPC listening at $ipcPath', name: kRpcIpcLogPrefix);
 | 
			
		||||
        if (Platform.isWindows) {
 | 
			
		||||
          _ipcServer = WindowsIpcServer();
 | 
			
		||||
        } else {
 | 
			
		||||
          _ipcServer = UnixIpcServer();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        _ipcServer!.listen((Socket socket) {
 | 
			
		||||
          _onIpcConnection(socket);
 | 
			
		||||
        });
 | 
			
		||||
        // Set up IPC handlers
 | 
			
		||||
        _ipcServer!.handlePacket = (socket, packet, _) {
 | 
			
		||||
          _handleIpcPacket(socket, packet);
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        await _ipcServer!.start();
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        developer.log('IPC server error: $e', name: kRpcIpcLogPrefix);
 | 
			
		||||
        // Continue without IPC if it fails
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      developer.log(
 | 
			
		||||
        'IPC server disabled on macOS in production mode due to sandboxing',
 | 
			
		||||
        'IPC server disabled on macOS or web in production mode',
 | 
			
		||||
        name: kRpcIpcLogPrefix,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
@@ -223,24 +137,23 @@ class ActivityRpcServer {
 | 
			
		||||
  Future<void> stop() async {
 | 
			
		||||
    // Stop WebSocket server
 | 
			
		||||
    for (var socket in _wsSockets) {
 | 
			
		||||
      try {
 | 
			
		||||
        await socket.sink.close();
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        developer.log('Error closing WebSocket: $e', name: kRpcLogPrefix);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    _wsSockets.clear();
 | 
			
		||||
    await _httpServer?.close();
 | 
			
		||||
    await _httpServer?.close(force: true);
 | 
			
		||||
 | 
			
		||||
    // Stop IPC server
 | 
			
		||||
    for (var socket in _ipcSockets) {
 | 
			
		||||
      socket.close();
 | 
			
		||||
    }
 | 
			
		||||
    _ipcSockets.clear();
 | 
			
		||||
    await _ipcServer?.close();
 | 
			
		||||
    await _ipcServer?.stop();
 | 
			
		||||
 | 
			
		||||
    developer.log('servers stopped', name: kRpcLogPrefix);
 | 
			
		||||
    developer.log('Servers stopped', name: kRpcLogPrefix);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle new WebSocket connection
 | 
			
		||||
  void _onWsConnection(WebSocketChannel socket, Request request) {
 | 
			
		||||
    // Parse query parameters
 | 
			
		||||
    final uri = request.url;
 | 
			
		||||
    final params = uri.queryParameters;
 | 
			
		||||
    final ver = int.tryParse(params['v'] ?? '1') ?? 1;
 | 
			
		||||
@@ -249,43 +162,38 @@ class ActivityRpcServer {
 | 
			
		||||
    final origin = request.headers['origin'] ?? '';
 | 
			
		||||
 | 
			
		||||
    developer.log(
 | 
			
		||||
      'new WS connection! origin: $origin, params: $params',
 | 
			
		||||
      'New WS connection! origin: $origin, params: $params',
 | 
			
		||||
      name: kRpcLogPrefix,
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Validate origin
 | 
			
		||||
    if (origin.isNotEmpty &&
 | 
			
		||||
        ![
 | 
			
		||||
          'https://discord.com',
 | 
			
		||||
          'https://ptb.discord.com',
 | 
			
		||||
          'https://canary.discord.com',
 | 
			
		||||
        ].contains(origin)) {
 | 
			
		||||
      developer.log('disallowed origin: $origin', name: kRpcLogPrefix);
 | 
			
		||||
      developer.log('Disallowed origin: $origin', name: kRpcLogPrefix);
 | 
			
		||||
      socket.sink.close();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Validate encoding
 | 
			
		||||
    if (encoding != 'json') {
 | 
			
		||||
      developer.log(
 | 
			
		||||
        'unsupported encoding requested: $encoding',
 | 
			
		||||
        'Unsupported encoding requested: $encoding',
 | 
			
		||||
        name: kRpcLogPrefix,
 | 
			
		||||
      );
 | 
			
		||||
      socket.sink.close();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Validate version
 | 
			
		||||
    if (ver != 1) {
 | 
			
		||||
      developer.log('unsupported version requested: $ver', name: kRpcLogPrefix);
 | 
			
		||||
      developer.log('Unsupported version requested: $ver', name: kRpcLogPrefix);
 | 
			
		||||
      socket.sink.close();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Store client info on socket
 | 
			
		||||
    final socketWithMeta = _WsSocketWrapper(socket, clientId, encoding);
 | 
			
		||||
 | 
			
		||||
    // Set up event listeners
 | 
			
		||||
    socket.stream.listen(
 | 
			
		||||
      (data) => _onWsMessage(socketWithMeta, data),
 | 
			
		||||
      onError: (e) {
 | 
			
		||||
@@ -298,36 +206,27 @@ class ActivityRpcServer {
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    // Notify handler of new connection
 | 
			
		||||
    handlers['connection']?.call(socketWithMeta);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle new IPC connection
 | 
			
		||||
  void _onIpcConnection(Socket socket) {
 | 
			
		||||
    developer.log('new IPC connection!', name: kRpcIpcLogPrefix);
 | 
			
		||||
 | 
			
		||||
    final socketWrapper = _IpcSocketWrapper(socket);
 | 
			
		||||
    _ipcSockets.add(socketWrapper);
 | 
			
		||||
 | 
			
		||||
    // Set up event listeners
 | 
			
		||||
    socket.listen(
 | 
			
		||||
      (data) => _onIpcData(socketWrapper, data),
 | 
			
		||||
      onError: (e) {
 | 
			
		||||
        developer.log('IPC socket error: $e', name: kRpcIpcLogPrefix);
 | 
			
		||||
        socket.close();
 | 
			
		||||
      },
 | 
			
		||||
      onDone: () {
 | 
			
		||||
        developer.log('IPC socket closed', name: kRpcIpcLogPrefix);
 | 
			
		||||
        handlers['close']?.call(socketWrapper);
 | 
			
		||||
        _ipcSockets.remove(socketWrapper);
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle incoming WebSocket message
 | 
			
		||||
  void _onWsMessage(_WsSocketWrapper socket, dynamic data) {
 | 
			
		||||
  Future<void> _onWsMessage(_WsSocketWrapper socket, dynamic data) async {
 | 
			
		||||
    if (data is! String) {
 | 
			
		||||
      developer.log(
 | 
			
		||||
        'Invalid WebSocket message: not a string',
 | 
			
		||||
        name: kRpcLogPrefix,
 | 
			
		||||
      );
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    try {
 | 
			
		||||
      final jsonData = jsonDecode(data as String);
 | 
			
		||||
      final jsonData = await compute(jsonDecode, data);
 | 
			
		||||
      if (jsonData is! Map<String, dynamic>) {
 | 
			
		||||
        developer.log(
 | 
			
		||||
          'Invalid WebSocket message: not a JSON object',
 | 
			
		||||
          name: kRpcLogPrefix,
 | 
			
		||||
        );
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      developer.log('WS message: $jsonData', name: kRpcLogPrefix);
 | 
			
		||||
      handlers['message']?.call(socket, jsonData);
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
@@ -335,22 +234,8 @@ class ActivityRpcServer {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle incoming IPC data
 | 
			
		||||
  void _onIpcData(_IpcSocketWrapper socket, List<int> data) {
 | 
			
		||||
    try {
 | 
			
		||||
      socket.addData(data);
 | 
			
		||||
      final packets = socket.readPackets();
 | 
			
		||||
      for (final packet in packets) {
 | 
			
		||||
        _handleIpcPacket(socket, packet);
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
 | 
			
		||||
      socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle IPC packet
 | 
			
		||||
  void _handleIpcPacket(_IpcSocketWrapper socket, _IpcPacket packet) {
 | 
			
		||||
  void _handleIpcPacket(IpcSocketWrapper socket, IpcPacket packet) {
 | 
			
		||||
    switch (packet.type) {
 | 
			
		||||
      case IpcTypes.ping:
 | 
			
		||||
        developer.log('IPC ping received', name: kRpcIpcLogPrefix);
 | 
			
		||||
@@ -359,7 +244,6 @@ class ActivityRpcServer {
 | 
			
		||||
 | 
			
		||||
      case IpcTypes.pong:
 | 
			
		||||
        developer.log('IPC pong received', name: kRpcIpcLogPrefix);
 | 
			
		||||
        // Handle pong if needed
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case IpcTypes.handshake:
 | 
			
		||||
@@ -388,13 +272,12 @@ class ActivityRpcServer {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle IPC handshake
 | 
			
		||||
  void _onIpcHandshake(_IpcSocketWrapper socket, Map<String, dynamic> params) {
 | 
			
		||||
  void _onIpcHandshake(IpcSocketWrapper socket, Map<String, dynamic> params) {
 | 
			
		||||
    developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix);
 | 
			
		||||
 | 
			
		||||
    final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
 | 
			
		||||
    final clientId = params['client_id']?.toString() ?? '';
 | 
			
		||||
 | 
			
		||||
    // Validate version
 | 
			
		||||
    if (ver != 1) {
 | 
			
		||||
      developer.log(
 | 
			
		||||
        'IPC unsupported version requested: $ver',
 | 
			
		||||
@@ -404,7 +287,6 @@ class ActivityRpcServer {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Validate client ID
 | 
			
		||||
    if (clientId.isEmpty) {
 | 
			
		||||
      developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
 | 
			
		||||
      socket.closeWithCode(IpcErrorCodes.invalidClientId);
 | 
			
		||||
@@ -413,7 +295,6 @@ class ActivityRpcServer {
 | 
			
		||||
 | 
			
		||||
    socket.clientId = clientId;
 | 
			
		||||
 | 
			
		||||
    // Notify handler of new connection
 | 
			
		||||
    handlers['connection']?.call(socket);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -432,74 +313,6 @@ class _WsSocketWrapper {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IPC wrapper
 | 
			
		||||
class _IpcSocketWrapper {
 | 
			
		||||
  final Socket socket;
 | 
			
		||||
  String clientId = '';
 | 
			
		||||
  bool handshook = false;
 | 
			
		||||
  final List<int> _buffer = [];
 | 
			
		||||
 | 
			
		||||
  _IpcSocketWrapper(this.socket);
 | 
			
		||||
 | 
			
		||||
  void addData(List<int> data) {
 | 
			
		||||
    _buffer.addAll(data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void send(Map<String, dynamic> msg) {
 | 
			
		||||
    developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
 | 
			
		||||
    final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.frame, msg);
 | 
			
		||||
    socket.add(packet);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void sendPong(dynamic data) {
 | 
			
		||||
    final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
 | 
			
		||||
    socket.add(packet);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void close() {
 | 
			
		||||
    socket.close();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void closeWithCode(int code, [String message = '']) {
 | 
			
		||||
    final closeData = {'code': code, 'message': message};
 | 
			
		||||
    final packet = ActivityRpcServer.encodeIpcPacket(IpcTypes.close, closeData);
 | 
			
		||||
    socket.add(packet);
 | 
			
		||||
    socket.close();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  List<_IpcPacket> readPackets() {
 | 
			
		||||
    final packets = <_IpcPacket>[];
 | 
			
		||||
 | 
			
		||||
    while (_buffer.length >= 8) {
 | 
			
		||||
      final buffer = Uint8List.fromList(_buffer);
 | 
			
		||||
      final byteData = ByteData.view(buffer.buffer);
 | 
			
		||||
 | 
			
		||||
      final type = byteData.getInt32(0, Endian.little);
 | 
			
		||||
      final dataSize = byteData.getInt32(4, Endian.little);
 | 
			
		||||
 | 
			
		||||
      if (_buffer.length < 8 + dataSize) break;
 | 
			
		||||
 | 
			
		||||
      final dataBytes = _buffer.sublist(8, 8 + dataSize);
 | 
			
		||||
      final jsonStr = utf8.decode(dataBytes);
 | 
			
		||||
      final jsonData = jsonDecode(jsonStr);
 | 
			
		||||
 | 
			
		||||
      packets.add(_IpcPacket(type, jsonData));
 | 
			
		||||
 | 
			
		||||
      _buffer.removeRange(0, 8 + dataSize);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return packets;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IPC Packet structure
 | 
			
		||||
class _IpcPacket {
 | 
			
		||||
  final int type;
 | 
			
		||||
  final Map<String, dynamic> data;
 | 
			
		||||
 | 
			
		||||
  _IpcPacket(this.type, this.data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// State management for server status and activities
 | 
			
		||||
class ServerState {
 | 
			
		||||
  final String status;
 | 
			
		||||
@@ -522,7 +335,6 @@ class ServerStateNotifier extends StateNotifier<ServerState> {
 | 
			
		||||
    : super(ServerState(status: 'Server not started'));
 | 
			
		||||
 | 
			
		||||
  Future<void> start() async {
 | 
			
		||||
    // Only start server on desktop platforms
 | 
			
		||||
    if (!Platform.isAndroid && !Platform.isIOS && !kIsWeb) {
 | 
			
		||||
      try {
 | 
			
		||||
        await server.start();
 | 
			
		||||
@@ -554,9 +366,8 @@ final rpcServerStateProvider =
 | 
			
		||||
          final clientId =
 | 
			
		||||
              socket is _WsSocketWrapper
 | 
			
		||||
                  ? socket.clientId
 | 
			
		||||
                  : (socket as _IpcSocketWrapper).clientId;
 | 
			
		||||
                  : (socket as IpcSocketWrapper).clientId;
 | 
			
		||||
          notifier.updateStatus('Client connected (ID: $clientId)');
 | 
			
		||||
          // Send READY event
 | 
			
		||||
          socket.send({
 | 
			
		||||
            'cmd': 'DISPATCH',
 | 
			
		||||
            'data': {
 | 
			
		||||
@@ -575,7 +386,7 @@ final rpcServerStateProvider =
 | 
			
		||||
              },
 | 
			
		||||
            },
 | 
			
		||||
            'evt': 'READY',
 | 
			
		||||
            'nonce': '12345', // Should be dynamic
 | 
			
		||||
            'nonce': '12345',
 | 
			
		||||
          });
 | 
			
		||||
        },
 | 
			
		||||
        'message': (socket, dynamic data) async {
 | 
			
		||||
@@ -583,7 +394,6 @@ final rpcServerStateProvider =
 | 
			
		||||
            notifier.addActivity(
 | 
			
		||||
              'Activity: ${data['args']['activity']['details'] ?? 'Unknown'}',
 | 
			
		||||
            );
 | 
			
		||||
            // Call setRemoteActivityStatus
 | 
			
		||||
            final label = data['args']['activity']['details'] ?? 'Unknown';
 | 
			
		||||
            final appId = socket.clientId;
 | 
			
		||||
            try {
 | 
			
		||||
@@ -594,7 +404,6 @@ final rpcServerStateProvider =
 | 
			
		||||
                name: kRpcLogPrefix,
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
            // Echo back success
 | 
			
		||||
            socket.send({
 | 
			
		||||
              'cmd': 'SET_ACTIVITY',
 | 
			
		||||
              'data': data['args']['activity'],
 | 
			
		||||
							
								
								
									
										118
									
								
								lib/pods/activity/ipc_server.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								lib/pods/activity/ipc_server.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,118 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
import 'dart:convert';
 | 
			
		||||
import 'dart:typed_data';
 | 
			
		||||
 | 
			
		||||
const String kRpcIpcLogPrefix = 'arRPC.ipc';
 | 
			
		||||
 | 
			
		||||
// IPC Packet Types
 | 
			
		||||
class IpcTypes {
 | 
			
		||||
  static const int handshake = 0;
 | 
			
		||||
  static const int frame = 1;
 | 
			
		||||
  static const int close = 2;
 | 
			
		||||
  static const int ping = 3;
 | 
			
		||||
  static const int pong = 4;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IPC Close Codes
 | 
			
		||||
class IpcCloseCodes {
 | 
			
		||||
  static const int closeNormal = 1000;
 | 
			
		||||
  static const int closeUnsupported = 1003;
 | 
			
		||||
  static const int closeAbnormal = 1006;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IPC Error Codes
 | 
			
		||||
class IpcErrorCodes {
 | 
			
		||||
  static const int invalidClientId = 4000;
 | 
			
		||||
  static const int invalidOrigin = 4001;
 | 
			
		||||
  static const int rateLimited = 4002;
 | 
			
		||||
  static const int tokenRevoked = 4003;
 | 
			
		||||
  static const int invalidVersion = 4004;
 | 
			
		||||
  static const int invalidEncoding = 4005;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// IPC Packet structure
 | 
			
		||||
class IpcPacket {
 | 
			
		||||
  final int type;
 | 
			
		||||
  final Map<String, dynamic> data;
 | 
			
		||||
 | 
			
		||||
  IpcPacket(this.type, this.data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Abstract base class for IPC server
 | 
			
		||||
abstract class IpcServer {
 | 
			
		||||
  final List<IpcSocketWrapper> _sockets = [];
 | 
			
		||||
 | 
			
		||||
  // Encode IPC packet
 | 
			
		||||
  static Uint8List encodeIpcPacket(int type, Map<String, dynamic> data) {
 | 
			
		||||
    final jsonData = jsonEncode(data);
 | 
			
		||||
    final dataBytes = utf8.encode(jsonData);
 | 
			
		||||
    final dataSize = dataBytes.length;
 | 
			
		||||
 | 
			
		||||
    final buffer = ByteData(8 + dataSize);
 | 
			
		||||
    buffer.setInt32(0, type, Endian.little);
 | 
			
		||||
    buffer.setInt32(4, dataSize, Endian.little);
 | 
			
		||||
    buffer.buffer.asUint8List().setRange(8, 8 + dataSize, dataBytes);
 | 
			
		||||
 | 
			
		||||
    return buffer.buffer.asUint8List();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<void> start();
 | 
			
		||||
  Future<void> stop();
 | 
			
		||||
 | 
			
		||||
  void addSocket(IpcSocketWrapper socket) {
 | 
			
		||||
    _sockets.add(socket);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void removeSocket(IpcSocketWrapper socket) {
 | 
			
		||||
    _sockets.remove(socket);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  List<IpcSocketWrapper> get sockets => _sockets;
 | 
			
		||||
 | 
			
		||||
  void Function(
 | 
			
		||||
    IpcSocketWrapper socket,
 | 
			
		||||
    IpcPacket packet,
 | 
			
		||||
    Map<String, Function> handlers,
 | 
			
		||||
  )?
 | 
			
		||||
  handlePacket;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Abstract base class for IPC socket wrapper
 | 
			
		||||
abstract class IpcSocketWrapper {
 | 
			
		||||
  String clientId = '';
 | 
			
		||||
  bool handshook = false;
 | 
			
		||||
  final List<int> _buffer = [];
 | 
			
		||||
 | 
			
		||||
  void addData(List<int> data) {
 | 
			
		||||
    _buffer.addAll(data);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void send(Map<String, dynamic> msg);
 | 
			
		||||
  void sendPong(dynamic data);
 | 
			
		||||
  void close();
 | 
			
		||||
  void closeWithCode(int code, [String message = '']);
 | 
			
		||||
 | 
			
		||||
  List<IpcPacket> readPackets() {
 | 
			
		||||
    final packets = <IpcPacket>[];
 | 
			
		||||
 | 
			
		||||
    while (_buffer.length >= 8) {
 | 
			
		||||
      final buffer = Uint8List.fromList(_buffer);
 | 
			
		||||
      final byteData = ByteData.view(buffer.buffer);
 | 
			
		||||
 | 
			
		||||
      final type = byteData.getInt32(0, Endian.little);
 | 
			
		||||
      final dataSize = byteData.getInt32(4, Endian.little);
 | 
			
		||||
 | 
			
		||||
      if (_buffer.length < 8 + dataSize) break;
 | 
			
		||||
 | 
			
		||||
      final dataBytes = _buffer.sublist(8, 8 + dataSize);
 | 
			
		||||
      final jsonStr = utf8.decode(dataBytes);
 | 
			
		||||
      final jsonData = jsonDecode(jsonStr);
 | 
			
		||||
 | 
			
		||||
      packets.add(IpcPacket(type, jsonData));
 | 
			
		||||
 | 
			
		||||
      _buffer.removeRange(0, 8 + dataSize);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return packets;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										203
									
								
								lib/pods/activity/ipc_server.unix.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										203
									
								
								lib/pods/activity/ipc_server.unix.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,203 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
import 'dart:developer' as developer;
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'package:path/path.dart' as path;
 | 
			
		||||
import 'ipc_server.dart';
 | 
			
		||||
 | 
			
		||||
class UnixIpcServer extends IpcServer {
 | 
			
		||||
  ServerSocket? _ipcServer;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<void> start() async {
 | 
			
		||||
    final ipcPath = await _findAvailableIpcPath();
 | 
			
		||||
    _ipcServer = await ServerSocket.bind(
 | 
			
		||||
      InternetAddress(ipcPath, type: InternetAddressType.unix),
 | 
			
		||||
      0,
 | 
			
		||||
    );
 | 
			
		||||
    developer.log('IPC listening at $ipcPath', name: kRpcIpcLogPrefix);
 | 
			
		||||
 | 
			
		||||
    _ipcServer!.listen((Socket socket) {
 | 
			
		||||
      _onIpcConnection(socket);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<void> stop() async {
 | 
			
		||||
    for (var socket in sockets) {
 | 
			
		||||
      try {
 | 
			
		||||
        socket.close();
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        developer.log('Error closing IPC socket: $e', name: kRpcIpcLogPrefix);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    sockets.clear();
 | 
			
		||||
    await _ipcServer?.close();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle new IPC connection
 | 
			
		||||
  void _onIpcConnection(Socket socket) {
 | 
			
		||||
    developer.log('New IPC connection!', name: kRpcIpcLogPrefix);
 | 
			
		||||
 | 
			
		||||
    final socketWrapper = UnixIpcSocketWrapper(socket);
 | 
			
		||||
    addSocket(socketWrapper);
 | 
			
		||||
 | 
			
		||||
    socket.listen(
 | 
			
		||||
      (data) => _onIpcData(socketWrapper, data),
 | 
			
		||||
      onError: (e) {
 | 
			
		||||
        developer.log('IPC socket error: $e', name: kRpcIpcLogPrefix);
 | 
			
		||||
        socket.close();
 | 
			
		||||
      },
 | 
			
		||||
      onDone: () {
 | 
			
		||||
        developer.log('IPC socket closed', name: kRpcIpcLogPrefix);
 | 
			
		||||
        removeSocket(socketWrapper);
 | 
			
		||||
      },
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle incoming IPC data
 | 
			
		||||
  void _onIpcData(UnixIpcSocketWrapper socket, List<int> data) {
 | 
			
		||||
    try {
 | 
			
		||||
      socket.addData(data);
 | 
			
		||||
      final packets = socket.readPackets();
 | 
			
		||||
      for (final packet in packets) {
 | 
			
		||||
        handlePacket?.call(socket, packet, {});
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
 | 
			
		||||
      socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle IPC handshake
 | 
			
		||||
  void _onIpcHandshake(
 | 
			
		||||
    IpcSocketWrapper socket,
 | 
			
		||||
    Map<String, dynamic> params,
 | 
			
		||||
    Map<String, Function> handlers,
 | 
			
		||||
  ) {
 | 
			
		||||
    developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix);
 | 
			
		||||
 | 
			
		||||
    final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
 | 
			
		||||
    final clientId = params['client_id']?.toString() ?? '';
 | 
			
		||||
 | 
			
		||||
    if (ver != 1) {
 | 
			
		||||
      developer.log(
 | 
			
		||||
        'IPC unsupported version requested: $ver',
 | 
			
		||||
        name: kRpcIpcLogPrefix,
 | 
			
		||||
      );
 | 
			
		||||
      socket.closeWithCode(IpcErrorCodes.invalidVersion);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (clientId.isEmpty) {
 | 
			
		||||
      developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
 | 
			
		||||
      socket.closeWithCode(IpcErrorCodes.invalidClientId);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    socket.clientId = clientId;
 | 
			
		||||
 | 
			
		||||
    handlers['connection']?.call(socket);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  Future<String> _getMacOsSystemTmpDir() async {
 | 
			
		||||
    final result = await Process.run('getconf', ['DARWIN_USER_TEMP_DIR']);
 | 
			
		||||
    return (result.stdout as String).trim();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Find available IPC socket path
 | 
			
		||||
  Future<String> _findAvailableIpcPath() async {
 | 
			
		||||
    // Build list of directories to try, with macOS-specific handling
 | 
			
		||||
    final baseDirs = <String>[];
 | 
			
		||||
 | 
			
		||||
    if (Platform.isMacOS) {
 | 
			
		||||
      try {
 | 
			
		||||
        final macTempDir = await _getMacOsSystemTmpDir();
 | 
			
		||||
        if (macTempDir.isNotEmpty) {
 | 
			
		||||
          baseDirs.add(macTempDir);
 | 
			
		||||
        }
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        developer.log(
 | 
			
		||||
          'Failed to get macOS system temp dir: $e',
 | 
			
		||||
          name: kRpcIpcLogPrefix,
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Add other standard directories
 | 
			
		||||
    final otherDirs = [
 | 
			
		||||
      Platform.environment['XDG_RUNTIME_DIR'],
 | 
			
		||||
      Platform.environment['TMPDIR'],
 | 
			
		||||
      Platform.environment['TMP'],
 | 
			
		||||
      Platform.environment['TEMP'],
 | 
			
		||||
      '/tmp',
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    baseDirs.addAll(
 | 
			
		||||
      otherDirs.where((dir) => dir != null && dir.isNotEmpty).cast<String>(),
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    for (final baseDir in baseDirs) {
 | 
			
		||||
      for (int i = 0; i < 10; i++) {
 | 
			
		||||
        final socketPath = path.join(baseDir, 'discord-ipc-$i');
 | 
			
		||||
        try {
 | 
			
		||||
          final socket = await ServerSocket.bind(
 | 
			
		||||
            InternetAddress(socketPath, type: InternetAddressType.unix),
 | 
			
		||||
            0,
 | 
			
		||||
          );
 | 
			
		||||
          socket.close();
 | 
			
		||||
          try {
 | 
			
		||||
            await File(socketPath).delete();
 | 
			
		||||
          } catch (_) {}
 | 
			
		||||
          developer.log(
 | 
			
		||||
            'IPC socket will be created at: $socketPath',
 | 
			
		||||
            name: kRpcIpcLogPrefix,
 | 
			
		||||
          );
 | 
			
		||||
          return socketPath;
 | 
			
		||||
        } catch (e) {
 | 
			
		||||
          if (i == 0) {
 | 
			
		||||
            developer.log(
 | 
			
		||||
              'IPC path $socketPath not available: $e',
 | 
			
		||||
              name: kRpcIpcLogPrefix,
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
          continue;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    throw Exception(
 | 
			
		||||
      'No available IPC socket paths found in any temp directory',
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class UnixIpcSocketWrapper extends IpcSocketWrapper {
 | 
			
		||||
  final Socket socket;
 | 
			
		||||
 | 
			
		||||
  UnixIpcSocketWrapper(this.socket);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void send(Map<String, dynamic> msg) {
 | 
			
		||||
    developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
 | 
			
		||||
    final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg);
 | 
			
		||||
    socket.add(packet);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void sendPong(dynamic data) {
 | 
			
		||||
    final packet = IpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
 | 
			
		||||
    socket.add(packet);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void close() {
 | 
			
		||||
    socket.close();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void closeWithCode(int code, [String message = '']) {
 | 
			
		||||
    final closeData = {'code': code, 'message': message};
 | 
			
		||||
    final packet = IpcServer.encodeIpcPacket(IpcTypes.close, closeData);
 | 
			
		||||
    socket.add(packet);
 | 
			
		||||
    socket.close();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										273
									
								
								lib/pods/activity/ipc_server.windows.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										273
									
								
								lib/pods/activity/ipc_server.windows.dart
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,273 @@
 | 
			
		||||
import 'dart:async';
 | 
			
		||||
import 'dart:developer' as developer;
 | 
			
		||||
import 'dart:ffi';
 | 
			
		||||
import 'dart:io';
 | 
			
		||||
import 'dart:isolate';
 | 
			
		||||
import 'package:ffi/ffi.dart';
 | 
			
		||||
import 'package:win32/win32.dart';
 | 
			
		||||
import 'ipc_server.dart';
 | 
			
		||||
 | 
			
		||||
class WindowsIpcServer extends IpcServer {
 | 
			
		||||
  int? _pipeHandle;
 | 
			
		||||
  Timer? _ipcTimer;
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<void> start() async {
 | 
			
		||||
    final pipeName = r'\\.\pipe\discord-ipc'.toNativeUtf16();
 | 
			
		||||
    try {
 | 
			
		||||
      _pipeHandle = CreateNamedPipe(
 | 
			
		||||
        pipeName,
 | 
			
		||||
        PIPE_ACCESS_DUPLEX,
 | 
			
		||||
        PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
 | 
			
		||||
        PIPE_UNLIMITED_INSTANCES,
 | 
			
		||||
        4096, // Output buffer size
 | 
			
		||||
        4096, // Input buffer size
 | 
			
		||||
        0, // Default timeout
 | 
			
		||||
        nullptr, // Security attributes
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      if (_pipeHandle == INVALID_HANDLE_VALUE) {
 | 
			
		||||
        final error = GetLastError();
 | 
			
		||||
        throw Exception('Failed to create named pipe: error code $error');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      developer.log(
 | 
			
		||||
        'IPC named pipe created at \\\\.\\pipe\\discord-ipc',
 | 
			
		||||
        name: kRpcIpcLogPrefix,
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // Start listening for connections in a separate isolate
 | 
			
		||||
      _listenWindowsIpc();
 | 
			
		||||
    } finally {
 | 
			
		||||
      free(pipeName);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  Future<void> stop() async {
 | 
			
		||||
    for (var socket in sockets) {
 | 
			
		||||
      try {
 | 
			
		||||
        socket.close();
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        developer.log('Error closing IPC socket: $e', name: kRpcIpcLogPrefix);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    sockets.clear();
 | 
			
		||||
 | 
			
		||||
    if (_pipeHandle != null) {
 | 
			
		||||
      try {
 | 
			
		||||
        CloseHandle(_pipeHandle!);
 | 
			
		||||
      } catch (e) {
 | 
			
		||||
        developer.log('Error closing named pipe: $e', name: kRpcIpcLogPrefix);
 | 
			
		||||
      }
 | 
			
		||||
      _pipeHandle = null;
 | 
			
		||||
    }
 | 
			
		||||
    _ipcTimer?.cancel();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Listen for Windows IPC connections in an isolate
 | 
			
		||||
  void _listenWindowsIpc() async {
 | 
			
		||||
    final receivePort = ReceivePort();
 | 
			
		||||
    await Isolate.spawn(_windowsIpcIsolate, receivePort.sendPort);
 | 
			
		||||
 | 
			
		||||
    receivePort.listen((message) {
 | 
			
		||||
      if (message is int) {
 | 
			
		||||
        final socketWrapper = WindowsIpcSocketWrapper(message);
 | 
			
		||||
        addSocket(socketWrapper);
 | 
			
		||||
        developer.log(
 | 
			
		||||
          'New IPC connection on named pipe',
 | 
			
		||||
          name: kRpcIpcLogPrefix,
 | 
			
		||||
        );
 | 
			
		||||
        _handleWindowsIpcData(socketWrapper);
 | 
			
		||||
        start(); // Create new pipe for next connection
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static void _windowsIpcIsolate(SendPort sendPort) {
 | 
			
		||||
    while (true) {
 | 
			
		||||
      final pipeHandle = CreateNamedPipe(
 | 
			
		||||
        r'\\.\pipe\discord-ipc'.toNativeUtf16(),
 | 
			
		||||
        PIPE_ACCESS_DUPLEX,
 | 
			
		||||
        PIPE_TYPE_MESSAGE | PIPE_READMODE_MESSAGE | PIPE_WAIT,
 | 
			
		||||
        PIPE_UNLIMITED_INSTANCES,
 | 
			
		||||
        4096,
 | 
			
		||||
        4096,
 | 
			
		||||
        0,
 | 
			
		||||
        nullptr,
 | 
			
		||||
      );
 | 
			
		||||
      if (pipeHandle == INVALID_HANDLE_VALUE) {
 | 
			
		||||
        developer.log(
 | 
			
		||||
          'Failed to create named pipe: ${GetLastError()}',
 | 
			
		||||
          name: kRpcIpcLogPrefix,
 | 
			
		||||
        );
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      final connected = ConnectNamedPipe(pipeHandle, nullptr);
 | 
			
		||||
      if (connected != 0 || GetLastError() == ERROR_PIPE_CONNECTED) {
 | 
			
		||||
        sendPort.send(pipeHandle);
 | 
			
		||||
      }
 | 
			
		||||
      // Avoid tight loop
 | 
			
		||||
      sleep(Duration(milliseconds: 100));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle Windows IPC data
 | 
			
		||||
  void _handleWindowsIpcData(WindowsIpcSocketWrapper socket) async {
 | 
			
		||||
    final startTime = DateTime.now();
 | 
			
		||||
    final buffer = malloc.allocate<BYTE>(4096);
 | 
			
		||||
    final bytesRead = malloc.allocate<DWORD>(4);
 | 
			
		||||
    try {
 | 
			
		||||
      while (socket.pipeHandle != null) {
 | 
			
		||||
        final readStart = DateTime.now();
 | 
			
		||||
        final success = ReadFile(
 | 
			
		||||
          socket.pipeHandle!,
 | 
			
		||||
          buffer.cast(),
 | 
			
		||||
          4096,
 | 
			
		||||
          bytesRead,
 | 
			
		||||
          nullptr,
 | 
			
		||||
        );
 | 
			
		||||
        final readDuration =
 | 
			
		||||
            DateTime.now().difference(readStart).inMicroseconds;
 | 
			
		||||
        developer.log(
 | 
			
		||||
          'ReadFile took $readDuration microseconds',
 | 
			
		||||
          name: kRpcIpcLogPrefix,
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        if (success == FALSE && GetLastError() != ERROR_MORE_DATA) {
 | 
			
		||||
          developer.log(
 | 
			
		||||
            'IPC read error: ${GetLastError()}',
 | 
			
		||||
            name: kRpcIpcLogPrefix,
 | 
			
		||||
          );
 | 
			
		||||
          socket.close();
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        final data = buffer.asTypedList(0);
 | 
			
		||||
        socket.addData(data);
 | 
			
		||||
        final packets = socket.readPackets();
 | 
			
		||||
        for (final packet in packets) {
 | 
			
		||||
          handlePacket?.call(socket, packet, {});
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    } catch (e) {
 | 
			
		||||
      developer.log('IPC data error: $e', name: kRpcIpcLogPrefix);
 | 
			
		||||
      socket.closeWithCode(IpcCloseCodes.closeUnsupported, e.toString());
 | 
			
		||||
    } finally {
 | 
			
		||||
      malloc.free(buffer);
 | 
			
		||||
      malloc.free(bytesRead);
 | 
			
		||||
      final totalDuration = DateTime.now().difference(startTime).inMicroseconds;
 | 
			
		||||
      developer.log(
 | 
			
		||||
        'handleWindowsIpcData took $totalDuration microseconds',
 | 
			
		||||
        name: kRpcIpcLogPrefix,
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle IPC handshake
 | 
			
		||||
  void _onIpcHandshake(
 | 
			
		||||
    IpcSocketWrapper socket,
 | 
			
		||||
    Map<String, dynamic> params,
 | 
			
		||||
    Map<String, Function> handlers,
 | 
			
		||||
  ) {
 | 
			
		||||
    developer.log('IPC handshake: $params', name: kRpcIpcLogPrefix);
 | 
			
		||||
 | 
			
		||||
    final ver = int.tryParse(params['v']?.toString() ?? '1') ?? 1;
 | 
			
		||||
    final clientId = params['client_id']?.toString() ?? '';
 | 
			
		||||
 | 
			
		||||
    if (ver != 1) {
 | 
			
		||||
      developer.log(
 | 
			
		||||
        'IPC unsupported version requested: $ver',
 | 
			
		||||
        name: kRpcIpcLogPrefix,
 | 
			
		||||
      );
 | 
			
		||||
      socket.closeWithCode(IpcErrorCodes.invalidVersion);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (clientId.isEmpty) {
 | 
			
		||||
      developer.log('IPC client ID required', name: kRpcIpcLogPrefix);
 | 
			
		||||
      socket.closeWithCode(IpcErrorCodes.invalidClientId);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    socket.clientId = clientId;
 | 
			
		||||
 | 
			
		||||
    handlers['connection']?.call(socket);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class WindowsIpcSocketWrapper extends IpcSocketWrapper {
 | 
			
		||||
  final int? pipeHandle;
 | 
			
		||||
 | 
			
		||||
  WindowsIpcSocketWrapper(this.pipeHandle);
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void send(Map<String, dynamic> msg) {
 | 
			
		||||
    developer.log('IPC sending: $msg', name: kRpcIpcLogPrefix);
 | 
			
		||||
    final packet = IpcServer.encodeIpcPacket(IpcTypes.frame, msg);
 | 
			
		||||
    final buffer = malloc.allocate<BYTE>(packet.length);
 | 
			
		||||
    buffer.asTypedList(packet.length).setAll(0, packet);
 | 
			
		||||
    final bytesWritten = malloc.allocate<DWORD>(4); // DWORD is 4 bytes
 | 
			
		||||
    try {
 | 
			
		||||
      WriteFile(
 | 
			
		||||
        pipeHandle!,
 | 
			
		||||
        buffer.cast(),
 | 
			
		||||
        packet.length,
 | 
			
		||||
        bytesWritten,
 | 
			
		||||
        nullptr,
 | 
			
		||||
      );
 | 
			
		||||
    } finally {
 | 
			
		||||
      malloc.free(buffer);
 | 
			
		||||
      malloc.free(bytesWritten);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void sendPong(dynamic data) {
 | 
			
		||||
    final packet = IpcServer.encodeIpcPacket(IpcTypes.pong, data ?? {});
 | 
			
		||||
    final buffer = malloc.allocate<BYTE>(packet.length);
 | 
			
		||||
    buffer.asTypedList(packet.length).setAll(0, packet);
 | 
			
		||||
    final bytesWritten = malloc.allocate<DWORD>(4); // DWORD is 4 bytes
 | 
			
		||||
    try {
 | 
			
		||||
      WriteFile(
 | 
			
		||||
        pipeHandle!,
 | 
			
		||||
        buffer.cast(),
 | 
			
		||||
        packet.length,
 | 
			
		||||
        bytesWritten,
 | 
			
		||||
        nullptr,
 | 
			
		||||
      );
 | 
			
		||||
    } finally {
 | 
			
		||||
      malloc.free(buffer);
 | 
			
		||||
      malloc.free(bytesWritten);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void close() {
 | 
			
		||||
    if (pipeHandle != null) {
 | 
			
		||||
      CloseHandle(pipeHandle!);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @override
 | 
			
		||||
  void closeWithCode(int code, [String message = '']) {
 | 
			
		||||
    final closeData = {'code': code, 'message': message};
 | 
			
		||||
    final packet = IpcServer.encodeIpcPacket(IpcTypes.close, closeData);
 | 
			
		||||
    final buffer = malloc.allocate<BYTE>(packet.length);
 | 
			
		||||
    buffer.asTypedList(packet.length).setAll(0, packet);
 | 
			
		||||
    final bytesWritten = malloc.allocate<DWORD>(4); // DWORD is 4 bytes
 | 
			
		||||
    try {
 | 
			
		||||
      WriteFile(
 | 
			
		||||
        pipeHandle!,
 | 
			
		||||
        buffer.cast(),
 | 
			
		||||
        packet.length,
 | 
			
		||||
        bytesWritten,
 | 
			
		||||
        nullptr,
 | 
			
		||||
      );
 | 
			
		||||
    } finally {
 | 
			
		||||
      malloc.free(buffer);
 | 
			
		||||
      malloc.free(bytesWritten);
 | 
			
		||||
    }
 | 
			
		||||
    CloseHandle(pipeHandle!);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -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';
 | 
			
		||||
 
 | 
			
		||||
@@ -48,11 +48,12 @@ class TrayService {
 | 
			
		||||
  void handleAction(MenuItem item) {
 | 
			
		||||
    switch (item.key) {
 | 
			
		||||
      case 'show_window':
 | 
			
		||||
        if (appWindow.isVisible) {
 | 
			
		||||
          appWindow.restore();
 | 
			
		||||
        } else {
 | 
			
		||||
        () async {
 | 
			
		||||
        appWindow.show();
 | 
			
		||||
        }
 | 
			
		||||
        appWindow.restore();
 | 
			
		||||
        await Future.delayed(const Duration(milliseconds: 32));
 | 
			
		||||
        appWindow.show();
 | 
			
		||||
        }();
 | 
			
		||||
        break;
 | 
			
		||||
      case 'exit_app':
 | 
			
		||||
        appWindow.close();
 | 
			
		||||
 
 | 
			
		||||
@@ -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<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);
 | 
			
		||||
  if (Platform.isWindows) {
 | 
			
		||||
    return windows_notify.initializeLocalNotifications();
 | 
			
		||||
  } 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);
 | 
			
		||||
    return universal_notify.initializeLocalNotifications();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
StreamSubscription<WebSocketPacket> 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'],
 | 
			
		||||
                );
 | 
			
		||||
  if (Platform.isWindows) {
 | 
			
		||||
    return windows_notify.setupNotificationListener(context, ref);
 | 
			
		||||
  } else {
 | 
			
		||||
                // External URLs
 | 
			
		||||
                launchUrlString(uri);
 | 
			
		||||
    return universal_notify.setupNotificationListener(context, ref);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
          },
 | 
			
		||||
          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(
 | 
			
		||||
  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 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},
 | 
			
		||||
  } else {
 | 
			
		||||
    return universal_notify.subscribePushNotification(
 | 
			
		||||
      apiClient,
 | 
			
		||||
      detailedErrors: detailedErrors,
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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},
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@@ -3,7 +3,7 @@ import 'package:bitsdojo_window/bitsdojo_window.dart';
 | 
			
		||||
import 'package:flutter/material.dart';
 | 
			
		||||
import 'package:flutter_hooks/flutter_hooks.dart';
 | 
			
		||||
import 'package:hooks_riverpod/hooks_riverpod.dart';
 | 
			
		||||
import 'package:island/pods/activity_rpc.dart';
 | 
			
		||||
import 'package:island/pods/activity/activity_rpc.dart';
 | 
			
		||||
import 'package:island/pods/websocket.dart';
 | 
			
		||||
import 'package:island/screens/tray_manager.dart';
 | 
			
		||||
import 'package:island/services/notify.dart';
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								pubspec.lock
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								pubspec.lock
									
									
									
									
									
								
							@@ -546,7 +546,7 @@ packages:
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.3.3"
 | 
			
		||||
  ffi:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: ffi
 | 
			
		||||
      sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418"
 | 
			
		||||
@@ -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:
 | 
			
		||||
@@ -2765,7 +2765,7 @@ packages:
 | 
			
		||||
    source: hosted
 | 
			
		||||
    version: "1.3.0"
 | 
			
		||||
  win32:
 | 
			
		||||
    dependency: transitive
 | 
			
		||||
    dependency: "direct main"
 | 
			
		||||
    description:
 | 
			
		||||
      name: win32
 | 
			
		||||
      sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
 | 
			
		||||
@@ -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"
 | 
			
		||||
 
 | 
			
		||||
@@ -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,9 @@ dependencies:
 | 
			
		||||
  slide_countdown: ^2.0.2
 | 
			
		||||
  shelf: ^1.4.2
 | 
			
		||||
  shelf_web_socket: ^3.0.0
 | 
			
		||||
  windows_notification: ^1.3.0
 | 
			
		||||
  win32: ^5.14.0
 | 
			
		||||
  ffi: ^2.1.4
 | 
			
		||||
 | 
			
		||||
dev_dependencies:
 | 
			
		||||
  flutter_test:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
; ==================================================
 | 
			
		||||
#define AppVersion "3.2.0"
 | 
			
		||||
#define BuildNumber "124"
 | 
			
		||||
#define BuildNumber "132"
 | 
			
		||||
; ==================================================
 | 
			
		||||
 | 
			
		||||
#define FullVersion AppVersion + "." + BuildNumber
 | 
			
		||||
 
 | 
			
		||||
@@ -31,6 +31,7 @@
 | 
			
		||||
#include <tray_manager/tray_manager_plugin.h>
 | 
			
		||||
#include <url_launcher_windows/url_launcher_windows.h>
 | 
			
		||||
#include <volume_controller/volume_controller_plugin_c_api.h>
 | 
			
		||||
#include <windows_notification/windows_notification_plugin_c_api.h>
 | 
			
		||||
 | 
			
		||||
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"));
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
 | 
			
		||||
  tray_manager
 | 
			
		||||
  url_launcher_windows
 | 
			
		||||
  volume_controller
 | 
			
		||||
  windows_notification
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user