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/score_sync_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; Future? _connectOp; bool get isConnected => _cable != null && _channel != null; Future connect({ required String sessionId, required String deviceRole, }) async { if (_sessionId == sessionId && isConnected) return; final prev = _connectOp; if (prev != null) await prev.catchError((_) {}); _connectOp = _connect(sessionId: sessionId, deviceRole: deviceRole); final op = _connectOp!; try { await op; } finally { if (identical(_connectOp, op)) { _connectOp = null; } } } Future _connect({ required String sessionId, required String deviceRole, }) async { 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': final remote = ScoreState.fromJson(data); if (_ref.read(scoreSyncProvider).acceptCableScore(remote)) { _ref.read(scoreProvider.notifier).applyRemote(remote); } 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; case 'stream_event': final event = data['event'] as String?; if (event == 'paused') { _ref.read(sessionProvider.notifier).applyCommand('pause_stream'); } else if (event == 'resumed') { _ref.read(sessionProvider.notifier).applyCommand('resume_stream'); } break; case 'youtube_ready': _ref.read(sessionProvider.notifier).updateYoutubeLinks( youtubeWatchUrl: data['youtube_watch_url'] as String?, shareUrl: data['share_url'] as String?, youtubeBroadcastId: data['youtube_broadcast_id'] as String?, ); 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 { final channel = _channel; final cable = _cable; _channel = null; _cable = null; _sessionId = null; try { channel?.unsubscribe(); } catch (_) {} try { cable?.disconnect(); } catch (_) {} await Future.delayed(const Duration(milliseconds: 150)); _ref.read(sessionProvider.notifier).setConnected(false); } void dispose() { disconnect(); } String encodeQrPayload(Map payload) => jsonEncode(payload); }