import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:web_socket_channel/web_socket_channel.dart'; import 'package:mirror_protocol/mirror_protocol.dart'; final connectionProvider = NotifierProvider(ConnectionNotifier.new); final mirrorStateProvider = StateProvider((ref) => null); final dashboardConfigProvider = StateProvider((ref) => null); class ConnectionState { final bool connected; final bool connecting; final String? host; final String? error; final bool manualDisconnect; const ConnectionState({ this.connected = false, this.connecting = false, this.host, this.error, this.manualDisconnect = false, }); ConnectionState copyWith({ bool? connected, bool? connecting, String? host, String? error, bool clearError = false, bool? manualDisconnect, }) => ConnectionState( connected: connected ?? this.connected, connecting: connecting ?? this.connecting, host: host ?? this.host, error: clearError ? null : (error ?? this.error), manualDisconnect: manualDisconnect ?? this.manualDisconnect, ); } class ConnectionNotifier extends Notifier { WebSocketChannel? _channel; Timer? _reconnectTimer; @override ConnectionState build() => const ConnectionState(); Future connect(String host, {int port = 9090}) async { state = state.copyWith(connecting: true, clearError: true, manualDisconnect: false); try { final uri = Uri.parse('ws://$host:$port'); _channel = WebSocketChannel.connect(uri); await _channel!.ready; state = state.copyWith(connected: true, connecting: false, host: host); // Send registration _channel!.sink.add(RegisterMessage(role: 'remote').encode()); // Listen for messages _channel!.stream.listen( _handleMessage, onDone: () => _onDisconnected(), onError: (e) => _onDisconnected(error: e.toString()), ); } catch (e) { state = state.copyWith( connected: false, connecting: false, error: 'Failed to connect: $e', ); } } void _handleMessage(dynamic data) { try { final message = MirrorMessage.decode(data as String); switch (message) { case StateSync(state: final mirrorState): ref.read(mirrorStateProvider.notifier).state = mirrorState; case DashboardConfigResponse(layout: final layout): ref.read(dashboardConfigProvider.notifier).state = layout; case StatePatch(): break; case EventMessage(): break; default: break; } } catch (e) { print('Failed to parse message: $e'); } } void _onDisconnected({String? error}) { state = state.copyWith(connected: false, error: error); _channel = null; // Only auto-reconnect if not intentionally disconnected if (state.host != null && error != null) { _reconnectTimer?.cancel(); _reconnectTimer = Timer(const Duration(seconds: 3), () { if (state.host != null && !state.connected) { connect(state.host!); } }); } } void send(MirrorMessage message) { _channel?.sink.add(message.encode()); } void disconnect() { _reconnectTimer?.cancel(); _channel?.sink.close(); _channel = null; state = const ConnectionState(manualDisconnect: true); ref.read(mirrorStateProvider.notifier).state = null; } }