import 'dart:async'; import 'dart:convert'; import 'package:dio/dio.dart'; import 'package:get/get.dart'; import 'package:uuid/uuid.dart'; import '../../config/translations/strings_enum.dart'; import '../../pigeons/core_api.g.dart'; import '../../utils/haptic_feedback_manager.dart'; import '../../utils/log/logger.dart'; import '../components/ix_snackbar.dart'; import '../constants/enums.dart'; import '../data/models/api_exception.dart'; import '../data/models/failure.dart'; import '../data/models/vpn_message.dart'; import '../data/sp/ix_sp.dart'; import '../dialog/error_dialog.dart'; import '../dialog/feedback_bottom_sheet.dart'; import 'api_controller.dart'; class CoreController extends GetxService { final TAG = 'CoreController'; final _apiController = Get.find(); final _state = ConnectionState.disconnected.obs; ConnectionState get state => _state.value; set state(ConnectionState value) => _state.value = value; // 公开状态流供外部监听 Rx get stateStream => _state; final _timer = "00:00:00".obs; String get timer => _timer.value; set timer(String value) => _timer.value = value; // VPN 事件流订阅 StreamSubscription? _eventSubscription; CancelToken? _cancelToken; @override void onInit() { super.onInit(); _initCheckConnect(); _startListeningToEvents(); } @override void onClose() { super.onClose(); // 取消事件流订阅 _eventSubscription?.cancel(); _eventSubscription = null; } void _initCheckConnect() { CoreApi().isConnected().then((value) { if (value == true) { state = ConnectionState.connected; CoreApi().reconnect(); } else { state = ConnectionState.disconnected; } }); } void handleConnection() { if (state == ConnectionState.disconnected) { // 开始连接 - 轻微震动 state = ConnectionState.connecting; HapticFeedbackManager.connectionStart(); getDispatchInfo(); } else { // 断开连接 CoreApi().disconnect(); } } void selectLocationConnect() { if (state != ConnectionState.disconnected) { CoreApi().disconnect(); // 延迟300ms Future.delayed(const Duration(milliseconds: 300), () { state = ConnectionState.connecting; getDispatchInfo(); }); } else { handleConnection(); } } Future getDispatchInfo() async { // 如果正在请求中,取消当前请求 if (_cancelToken != null) { log(TAG, '取消当前请求,重新发起新请求'); _cancelToken?.cancel('取消旧请求,发起新请求'); } // 创建新的 CancelToken final currentToken = CancelToken(); _cancelToken = currentToken; try { final locationId = IXSP.getSelectedLocation()?['id']; final locationCode = IXSP.getSelectedLocation()?['code']; final launch = await _apiController.getDispatchInfo( locationId, locationCode, cancelToken: currentToken, ); // 只有当前 token 没有被替换时才清空 if (_cancelToken == currentToken) { _cancelToken = null; } if (state == ConnectionState.connecting) { final sessionId = Uuid().v4(); final socksPort = launch.nodesConfig!.socketPort!; final tunnelConfig = launch.nodesConfig!.tunnelConfig!; final configJson = jsonEncode(launch.nodesConfig!); CoreApi().connect(sessionId, socksPort, tunnelConfig, configJson); } } on DioException catch (e, s) { // 只有当前 token 没有被替换时才清空 if (_cancelToken == currentToken) { _cancelToken = null; } // 如果是取消错误,不处理 if (e.type == DioExceptionType.cancel) { log(TAG, '请求已取消'); return; } if (state == ConnectionState.connecting) { state = ConnectionState.disconnected; } handleErrorDialog(e, s); log(TAG, 'getDispatchInfo error: $e'); } catch (e, s) { // 只有当前 token 没有被替换时才清空 if (_cancelToken == currentToken) { _cancelToken = null; } if (state == ConnectionState.connecting) { state = ConnectionState.disconnected; } handleErrorDialog(e, s); log(TAG, 'getDispatchInfo error: $e'); } } /// 开始监听来自 Android 的事件 void _startListeningToEvents() { _eventSubscription = onEventChange().listen( _handleEventChange, onError: (error) { log(TAG, '事件流错误: $error'); }, ); } // 处理从原生端接收到的消息 void _handleEventChange(String message) { try { final Map json = jsonDecode(message); final String type = json['type'] ?? ''; switch (type) { case 'vpn_status': _handleVpnStatus(VpnStatusMessage.fromJson(json)); break; case 'timer_update': _handleTimerUpdate(TimerUpdateMessage.fromJson(json)); break; default: log(TAG, '未知消息类型: $type'); } } catch (e) { log(TAG, '解析消息失败: $e'); } } void _handleVpnStatus(VpnStatusMessage message) { final vpnError = VpnStatus.fromValue(message.status); log( TAG, 'VPN状态变化: ${vpnError.label}, status=${message.status}, message=${message.message}', ); // 根据状态码处理不同的VPN状态 switch (vpnError) { case VpnStatus.idle: // disconnected _onVpnDisconnected(); break; case VpnStatus.connecting: // connecting _onVpnConnecting(); break; case VpnStatus.connected: // connected _onVpnConnected(); break; case VpnStatus.error: // error _onVpnError(); break; case VpnStatus.serviceDisconnected: // service disconnected _onVpnServiceDisconnected(); break; case VpnStatus.permissionDenied: // permission denied _onVpnPermissionDenied(); break; } } void _handleTimerUpdate(TimerUpdateMessage message) { log( TAG, '计时更新: time=${message.currentTime}, mode=${message.mode}, running=${message.isRunning}, paused=${message.isPaused}', ); timer = _formatTime(message.currentTime); // 处理计时更新 if (message.isRunning) { if (message.isPaused) { _onTimerPaused(message.currentTime, message.mode); } else { _onTimerRunning(message.currentTime, message.mode); } } else { _onTimerStopped(); } } // VPN状态处理方法 void _onVpnDisconnected() { log(TAG, 'VPN已断开连接'); // 更新UI状态 state = ConnectionState.disconnected; timer = "00:00:00"; HapticFeedbackManager.connectionDisconnected(); FeedbackBottomSheet.show(); } void _onVpnConnecting() { log(TAG, 'VPN正在连接'); // 显示连接中状态 state = ConnectionState.connecting; } void _onVpnConnected() { log(TAG, 'VPN已连接'); // 显示已连接状态 state = ConnectionState.connected; HapticFeedbackManager.connectionSuccess(); } void _onVpnError() { log(TAG, 'VPN连接错误'); // 显示错误信息 state = ConnectionState.disconnected; timer = "00:00:00"; HapticFeedbackManager.connectionDisconnected(); ErrorDialog.show(message: Strings.vpnConnectionError.tr); } void _onVpnServiceDisconnected() { log(TAG, 'VPN服务异常断开连接'); // 显示错误信息 state = ConnectionState.disconnected; timer = "00:00:00"; HapticFeedbackManager.connectionDisconnected(); // 可以显示错误提示 ErrorDialog.show(message: Strings.vpnServiceDisconnected.tr); } void _onVpnPermissionDenied() { log(TAG, 'VPN权限拒绝'); // 显示权限拒绝状态 state = ConnectionState.disconnected; HapticFeedbackManager.connectionDisconnected(); // 可以显示错误提示 ErrorDialog.show(message: '权限拒绝'); } // 计时器状态处理方法 void _onTimerRunning(int currentTime, int mode) { log( TAG, '计时器运行中: ${_formatTime(currentTime)}, 模式: ${mode == 0 ? "普通计时" : "倒计时"}', ); } void _onTimerPaused(int currentTime, int mode) { log( TAG, '计时器已暂停: ${_formatTime(currentTime)}, 模式: ${mode == 0 ? "普通计时" : "倒计时"}', ); } void _onTimerStopped() { log(TAG, '计时器已停止'); } // 格式化时间显示 String _formatTime(int timeMs) { final totalSeconds = (timeMs / 1000).abs().round(); final days = totalSeconds ~/ 86400; // 86400 = 24 * 3600 final hours = (totalSeconds % 86400) ~/ 3600; final minutes = (totalSeconds % 3600) ~/ 60; final seconds = totalSeconds % 60; if (days > 0) { return '$days days ${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } else if (hours > 0) { return '${hours.toString().padLeft(2, '0')}:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } else { return '00:${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}'; } } void handleSnackBarError(dynamic error, StackTrace stackTrace) { if (error is ApiException) { IXSnackBar.showIXErrorSnackBar( title: Strings.error.tr, message: error.message, ); } else if (error is Failure) { IXSnackBar.showIXErrorSnackBar( title: Strings.error.tr, message: error.message ?? Strings.unknownError.tr, ); } else if (error is DioException) { switch (error.type) { case DioExceptionType.connectionError: case DioExceptionType.connectionTimeout: case DioExceptionType.receiveTimeout: case DioExceptionType.sendTimeout: IXSnackBar.showIXErrorSnackBar( title: Strings.error.tr, message: Strings.unableToConnectNetwork.tr, ); break; default: IXSnackBar.showIXErrorSnackBar( title: Strings.error.tr, message: Strings.unableToConnectServer.tr, ); } } else { IXSnackBar.showIXErrorSnackBar( title: Strings.error.tr, message: error.toString(), ); } } void handleErrorDialog(dynamic error, StackTrace stackTrace) { if (error is ApiException) { ErrorDialog.show(title: Strings.error.tr, message: error.message); } else if (error is Failure) { ErrorDialog.show( title: Strings.error.tr, message: error.message ?? Strings.unknownError.tr, ); } else if (error is DioException) { switch (error.type) { case DioExceptionType.connectionError: case DioExceptionType.connectionTimeout: case DioExceptionType.receiveTimeout: case DioExceptionType.sendTimeout: ErrorDialog.show( title: Strings.error.tr, message: Strings.unableToConnectNetwork.tr, ); break; default: ErrorDialog.show( title: Strings.error.tr, message: Strings.unableToConnectServer.tr, ); } } else { ErrorDialog.show(title: Strings.error.tr, message: error.toString()); } } }