import 'dart:async'; import 'dart:io'; import 'package:flutter_riverpod/flutter_riverpod.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:mirror_protocol/mirror_protocol.dart'; import '../providers/display_state_provider.dart'; import '../providers/layout_provider.dart'; import 'layout_persistence.dart'; final wsServerProvider = Provider((ref) => WsServer(ref)); class WsServer { final Ref _ref; final Set _clients = {}; HttpServer? _server; final int port; final LayoutPersistence _layoutPersistence = LayoutPersistence(); DashboardLayoutConfig? _currentLayout; WsServer(this._ref, {this.port = 9090}); Future start() async { _currentLayout = await _layoutPersistence.load(); final handler = webSocketHandler((WebSocketChannel channel) { _clients.add(channel); // Send current state on connect final state = _ref.read(displayStateProvider); final sync = StateSync(state: state); channel.sink.add(sync.encode()); channel.stream.listen( (data) => _handleMessage(channel, data as String), onDone: () => _clients.remove(channel), onError: (_) => _clients.remove(channel), ); }); _server = await shelf_io.serve(handler, InternetAddress.anyIPv4, port); print('WebSocket server running on :$port'); } void _handleMessage(WebSocketChannel sender, String raw) { try { final message = MirrorMessage.decode(raw); _routeMessage(sender, message); } catch (e) { print('Invalid message: $e'); } } void _routeMessage(WebSocketChannel sender, MirrorMessage message) { final notifier = _ref.read(displayStateProvider.notifier); switch (message) { case NavigateCommand(:final target): notifier.navigate(target); case PlaybackCommand(:final action, :final value): notifier.handlePlayback(action, value); case WorkoutCommand(:final action, :final payload): notifier.handleWorkout(action, payload); case ProgramCommand(:final action, :final payload): notifier.handleProgram(action, payload); case MediaCommand(:final action, :final payload): notifier.handleMedia(action, payload); case RegisterMessage(): // Already handled on connect break; case DashboardConfigRequest(): _handleConfigRequest(sender); case DashboardConfigUpdate(:final layout): _handleConfigUpdate(layout); default: break; } } void _handleConfigRequest(WebSocketChannel sender) { final layout = _currentLayout ?? DashboardLayoutConfig.defaultLayout(); final response = DashboardConfigResponse(layout: layout); try { sender.sink.add(response.encode()); } catch (_) { _clients.remove(sender); } } void _handleConfigUpdate(DashboardLayoutConfig layout) { _currentLayout = layout; _layoutPersistence.save(layout); // Update the layout provider so dashboard screen rebuilds _ref.read(layoutProvider.notifier).updateLayout(layout); } void broadcast(MirrorMessage message) { final encoded = message.encode(); for (final client in _clients.toList()) { try { client.sink.add(encoded); } catch (_) { _clients.remove(client); } } } void broadcastState(DisplayState state) { broadcast(StateSync(state: state)); } DashboardLayoutConfig get currentLayout => _currentLayout ?? DashboardLayoutConfig.defaultLayout(); Future stop() async { for (final client in _clients) { await client.sink.close(); } _clients.clear(); await _server?.close(); } }