import 'dart:convert'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:x_action_cable_v2/x_action_cable.dart'; import '../core/config.dart'; import '../providers/auth_provider.dart'; import '../providers/score_provider.dart'; import '../providers/session_provider.dart'; import 'models/device_state.dart'; import 'models/score_state.dart'; final websocketServiceProvider = Provider((ref) { final service = WebsocketService(ref); ref.onDispose(service.dispose); return service; }); typedef CableMessageHandler = void Function(Map data); class WebsocketService { WebsocketService(this._ref); final Ref _ref; ActionCable? _cable; ActionChannel? _channel; String? _sessionId; bool get isConnected => _cable != null && _channel != null; Future connect({ required String sessionId, required String deviceRole, }) async { if (_sessionId == sessionId && isConnected) return; await disconnect(); final token = _ref.read(authProvider).accessToken; if (token == null || token.isEmpty) { throw StateError('Token di accesso mancante'); } _sessionId = sessionId; _cable = ActionCable.connect( AppConfig.cableUrl, headers: {'Authorization': 'Bearer $token'}, onConnected: () { _ref.read(sessionProvider.notifier).setConnected(true); }, onConnectionLost: () { _ref.read(sessionProvider.notifier).setConnected(false); }, onCannotConnect: (_) { _ref.read(sessionProvider.notifier).setConnected(false); }, ); _channel = _cable!.subscribe( 'SessionChannel', channelParams: { 'session_id': sessionId, 'device_role': deviceRole, }, onSubscribed: () { _ref.read(sessionProvider.notifier).setConnected(true); }, onDisconnected: () { _ref.read(sessionProvider.notifier).setConnected(false); }, callbacks: [ ActionCallback( name: 'receive_message', callback: _handleMessage, ), ], ); } void _handleMessage(ActionResponse response) { if (response.hasError || response.data == null) return; _dispatch(Map.from(response.data!)); } void _dispatch(Map data) { final type = data['type'] as String?; switch (type) { case 'score_update': _ref.read(scoreProvider.notifier).applyRemote(ScoreState.fromJson(data)); break; case 'device_state': _ref .read(sessionProvider.notifier) .updateDeviceState(DeviceStateModel.fromJson(data)); break; case 'timeout': final team = data['team'] as String?; if (team == 'home') { _ref.read(scoreProvider.notifier).setTimeoutHome(true); } else if (team == 'away') { _ref.read(scoreProvider.notifier).setTimeoutAway(true); } break; case 'command': final action = data['action'] as String?; if (action != null) { _ref.read(sessionProvider.notifier).applyCommand(action); } break; default: break; } } void sendScoreUpdate(ScoreState score) { _perform({'type': 'score_update', ...score.toCablePayload()}); } void sendTimeout(String team) { _perform({'type': 'timeout', 'team': team}); } void sendCommand(String action) { _perform({'type': 'command', 'action': action}); } void sendDeviceState(DeviceStateModel state) { _perform({ 'type': 'device_state', 'device_role': state.deviceRole, 'battery': state.batteryLevel, 'network': state.networkType, 'bitrate': state.currentBitrate, 'fps': state.fps, }); } void _perform(Map payload) { _channel?.performAction('receive', params: payload); } Future disconnect() async { _channel?.unsubscribe(); _channel = null; _cable?.disconnect(); _cable = null; _sessionId = null; _ref.read(sessionProvider.notifier).setConnected(false); } void dispose() { disconnect(); } String encodeQrPayload(Map payload) => jsonEncode(payload); }