import 'dart:async'; import 'dart:convert'; import 'dart:io'; import 'dart:math'; import 'package:flutter/foundation.dart'; import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:network_info_plus/network_info_plus.dart'; import 'package:multicast_dns/multicast_dns.dart'; class NetworkUser { final String id; final String name; final String ipAddress; final DateTime joinedAt; final int votes; bool isOnline; DateTime lastSeen; NetworkUser({ required this.id, required this.name, required this.ipAddress, required this.joinedAt, this.votes = 0, this.isOnline = true, DateTime? lastSeen, }) : lastSeen = lastSeen ?? DateTime.now(); Map toJson() => { 'id': id, 'name': name, 'ipAddress': ipAddress, 'joinedAt': joinedAt.toIso8601String(), 'votes': votes, 'isOnline': isOnline, 'lastSeen': lastSeen.toIso8601String(), }; factory NetworkUser.fromJson(Map json) => NetworkUser( id: json['id'], name: json['name'], ipAddress: json['ipAddress'], joinedAt: DateTime.parse(json['joinedAt']), votes: json['votes'] ?? 0, isOnline: json['isOnline'] ?? true, lastSeen: DateTime.parse(json['lastSeen']), ); } class NetworkGroupService extends ChangeNotifier { static const int _discoveryPort = 8089; static const int _heartbeatInterval = 10; // seconds final Connectivity _connectivity = Connectivity(); final NetworkInfo _networkInfo = NetworkInfo(); MDnsClient? _mdnsClient; HttpServer? _httpServer; Timer? _heartbeatTimer; Timer? _discoveryTimer; bool _isConnectedToWifi = false; String _currentNetworkName = ''; String _currentNetworkSSID = ''; String _localIpAddress = ''; String _networkSubnet = ''; final Map _networkUsers = {}; late NetworkUser _currentUser; // Getters bool get isConnectedToWifi => _isConnectedToWifi; String get currentNetworkName => _currentNetworkName; String get currentNetworkSSID => _currentNetworkSSID; String get localIpAddress => _localIpAddress; List get networkUsers => _networkUsers.values.toList(); NetworkUser get currentUser => _currentUser; int get onlineUsersCount => _networkUsers.values.where((u) => u.isOnline).length; NetworkGroupService() { _initializeCurrentUser(); _startNetworkMonitoring(); } void _initializeCurrentUser() { final random = Random(); final guestNames = ['Alex', 'Sarah', 'Marco', 'Lisa', 'Tom', 'Anna', 'David', 'Emma']; final randomName = guestNames[random.nextInt(guestNames.length)]; final randomId = random.nextInt(999); _currentUser = NetworkUser( id: 'user_${DateTime.now().millisecondsSinceEpoch}', name: '$randomName #$randomId', ipAddress: _localIpAddress, joinedAt: DateTime.now(), ); } Future _startNetworkMonitoring() async { // Monitor connectivity changes _connectivity.onConnectivityChanged.listen(_onConnectivityChanged); // Initial connectivity check await _checkConnectivity(); } Future _onConnectivityChanged(List results) async { await _checkConnectivity(); } Future _checkConnectivity() async { final connectivityResult = await _connectivity.checkConnectivity(); final wasConnected = _isConnectedToWifi; _isConnectedToWifi = connectivityResult.contains(ConnectivityResult.wifi); if (_isConnectedToWifi) { await _getNetworkInfo(); if (!wasConnected) { await _startNetworkServices(); } } else { if (wasConnected) { await _stopNetworkServices(); } _clearNetworkInfo(); } notifyListeners(); } Future _getNetworkInfo() async { try { _currentNetworkSSID = await _networkInfo.getWifiName() ?? 'Unknown Network'; _currentNetworkName = _currentNetworkSSID.replaceAll('"', ''); // Remove quotes _localIpAddress = await _networkInfo.getWifiIP() ?? ''; // Calculate network subnet (assuming /24) if (_localIpAddress.isNotEmpty) { final parts = _localIpAddress.split('.'); if (parts.length == 4) { _networkSubnet = '${parts[0]}.${parts[1]}.${parts[2]}'; } } // Update current user's IP _currentUser = NetworkUser( id: _currentUser.id, name: _currentUser.name, ipAddress: _localIpAddress, joinedAt: _currentUser.joinedAt, votes: _currentUser.votes, ); } catch (e) { if (kDebugMode) { print('Error getting network info: $e'); } } } void _clearNetworkInfo() { _currentNetworkName = ''; _currentNetworkSSID = ''; _localIpAddress = ''; _networkSubnet = ''; _networkUsers.clear(); } Future _startNetworkServices() async { if (!_isConnectedToWifi || _localIpAddress.isEmpty) return; try { // Start mDNS client for service discovery _mdnsClient = MDnsClient(); await _mdnsClient!.start(); // Start HTTP server for peer communication await _startHttpServer(); // Start announcing our service await _announceService(); // Start discovering other users _startDiscovery(); // Start heartbeat for keeping users online _startHeartbeat(); // Add ourselves to the users list _networkUsers[_currentUser.id] = _currentUser; } catch (e) { if (kDebugMode) { print('Error starting network services: $e'); } } } Future _stopNetworkServices() async { // Stop timers _heartbeatTimer?.cancel(); _discoveryTimer?.cancel(); // Stop HTTP server await _httpServer?.close(); _httpServer = null; // Stop mDNS client _mdnsClient?.stop(); _mdnsClient = null; // Clear users _networkUsers.clear(); } Future _startHttpServer() async { try { _httpServer = await HttpServer.bind(InternetAddress.anyIPv4, _discoveryPort); _httpServer!.listen((request) async { await _handleHttpRequest(request); }); } catch (e) { if (kDebugMode) { print('Error starting HTTP server: $e'); } } } Future _handleHttpRequest(HttpRequest request) async { try { final response = request.response; response.headers.set('Content-Type', 'application/json'); response.headers.set('Access-Control-Allow-Origin', '*'); if (request.method == 'GET' && request.uri.path == '/user') { // Return current user info response.write(jsonEncode(_currentUser.toJson())); } else if (request.method == 'POST' && request.uri.path == '/heartbeat') { // Handle heartbeat from other users final body = await utf8.decoder.bind(request).join(); final userData = jsonDecode(body); final user = NetworkUser.fromJson(userData); _networkUsers[user.id] = user; notifyListeners(); response.write(jsonEncode({'status': 'ok'})); } else { response.statusCode = 404; response.write(jsonEncode({'error': 'Not found'})); } await response.close(); } catch (e) { if (kDebugMode) { print('Error handling HTTP request: $e'); } } } Future _announceService() async { if (_mdnsClient == null || _localIpAddress.isEmpty) return; try { // This would announce our service via mDNS // In a real implementation, you'd use proper mDNS announcements if (kDebugMode) { print('Announcing service on $_localIpAddress:$_discoveryPort'); } } catch (e) { if (kDebugMode) { print('Error announcing service: $e'); } } } void _startDiscovery() { _discoveryTimer = Timer.periodic(const Duration(seconds: 15), (_) async { await _discoverUsers(); }); // Initial discovery _discoverUsers(); } Future _discoverUsers() async { if (_networkSubnet.isEmpty) return; // Scan local network for other SleepySound users final futures = []; for (int i = 1; i <= 254; i++) { final ip = '$_networkSubnet.$i'; if (ip != _localIpAddress) { futures.add(_tryConnectToUser(ip)); } } // Wait for all connection attempts (with timeout) await Future.wait(futures).timeout( const Duration(seconds: 10), onTimeout: () => [], ); } Future _tryConnectToUser(String ip) async { try { final client = HttpClient(); client.connectionTimeout = const Duration(seconds: 2); final request = await client.getUrl(Uri.parse('http://$ip:$_discoveryPort/user')); final response = await request.close().timeout(const Duration(seconds: 2)); if (response.statusCode == 200) { final body = await utf8.decoder.bind(response).join(); final userData = jsonDecode(body); final user = NetworkUser.fromJson(userData); if (user.id != _currentUser.id) { _networkUsers[user.id] = user; notifyListeners(); } } client.close(); } catch (e) { // Ignore connection errors (expected for non-SleepySound devices) } } void _startHeartbeat() { _heartbeatTimer = Timer.periodic(const Duration(seconds: _heartbeatInterval), (_) async { await _sendHeartbeat(); _cleanupOfflineUsers(); }); } Future _sendHeartbeat() async { final heartbeatData = jsonEncode(_currentUser.toJson()); // Send heartbeat to all known users final futures = _networkUsers.values .where((user) => user.id != _currentUser.id && user.isOnline) .map((user) => _sendHeartbeatToUser(user.ipAddress, heartbeatData)); await Future.wait(futures); } Future _sendHeartbeatToUser(String ip, String data) async { try { final client = HttpClient(); client.connectionTimeout = const Duration(seconds: 2); final request = await client.postUrl(Uri.parse('http://$ip:$_discoveryPort/heartbeat')); request.headers.set('Content-Type', 'application/json'); request.write(data); await request.close().timeout(const Duration(seconds: 2)); client.close(); } catch (e) { // Mark user as potentially offline final user = _networkUsers.values.firstWhere( (u) => u.ipAddress == ip, orElse: () => NetworkUser(id: '', name: '', ipAddress: '', joinedAt: DateTime.now()), ); if (user.id.isNotEmpty) { user.isOnline = false; notifyListeners(); } } } void _cleanupOfflineUsers() { final now = DateTime.now(); final usersToRemove = []; for (final user in _networkUsers.values) { if (user.id != _currentUser.id) { final timeSinceLastSeen = now.difference(user.lastSeen).inSeconds; if (timeSinceLastSeen > _heartbeatInterval * 3) { user.isOnline = false; // Remove users that have been offline for more than 5 minutes if (timeSinceLastSeen > 300) { usersToRemove.add(user.id); } } } } for (final userId in usersToRemove) { _networkUsers.remove(userId); } if (usersToRemove.isNotEmpty) { notifyListeners(); } } // Public methods for UI interaction Future refreshNetwork() async { await _checkConnectivity(); if (_isConnectedToWifi) { await _discoverUsers(); } } void updateUserVotes(int votes) { _currentUser = NetworkUser( id: _currentUser.id, name: _currentUser.name, ipAddress: _currentUser.ipAddress, joinedAt: _currentUser.joinedAt, votes: votes, isOnline: true, ); _networkUsers[_currentUser.id] = _currentUser; notifyListeners(); } String getConnectionStatus() { if (!_isConnectedToWifi) { return 'Not connected to WiFi'; } if (_currentNetworkName.isEmpty) { return 'Connected to WiFi'; } return 'Connected to $_currentNetworkName'; } // Demo methods for testing void simulateNetworkConnection() { _isConnectedToWifi = true; _currentNetworkName = 'Demo WiFi Network'; _currentNetworkSSID = '"Demo WiFi Network"'; _localIpAddress = '192.168.1.100'; _networkSubnet = '192.168.1'; // Update current user with new IP _currentUser = NetworkUser( id: _currentUser.id, name: _currentUser.name, ipAddress: _localIpAddress, joinedAt: _currentUser.joinedAt, votes: _currentUser.votes, ); _networkUsers[_currentUser.id] = _currentUser; notifyListeners(); } void clearDemoUsers() { _networkUsers.clear(); _networkUsers[_currentUser.id] = _currentUser; notifyListeners(); } void addDemoUser(String name, int votes) { final user = NetworkUser( id: 'demo_${DateTime.now().millisecondsSinceEpoch}_${name.hashCode}', name: name, ipAddress: '192.168.1.${101 + _networkUsers.length}', joinedAt: DateTime.now().subtract(Duration(minutes: (votes * 2))), votes: votes, isOnline: true, ); _networkUsers[user.id] = user; notifyListeners(); } @override void dispose() { _stopNetworkServices(); super.dispose(); } }