// connection_provider.dart — 连接状态机(严格三态,状态由内核事件驱动) // // 设计约定: // - UI 调用 toggle(),控制器内部读取有效节点 + 认证令牌,调用 ConnectApi // 并启动 VpnBridge 子进程。 // - 严禁乐观翻转:VpnPhase.on 必须由 bridge.statusStream 确认后才置。 // - 状态来源:bridge.statusStream(来自内核回调),非 Timer 模拟。 import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import '../bridge/vpn_bridge.dart'; import '../bridge/vpn_bridge_provider.dart'; import '../services/connect_api.dart'; import 'auth_provider.dart'; import 'nodes_provider.dart'; // ── 设备 ID(MVP 常量;后续由 device_info_plus 取真实 ID)────────── const _kDeviceId = String.fromEnvironment( 'PANGOLIN_DEVICE_ID', defaultValue: 'mac-001', ); // ── API base URL ───────────────────────────────────────────────── const _kApiUrl = String.fromEnvironment( 'PANGOLIN_API_URL', defaultValue: 'http://localhost:8080', ); // ── 连接阶段枚举 ────────────────────────────────────────────────── /// 连接阶段。严格三态,与设计稿一致。 enum VpnPhase { off, connecting, on } // ── 连接状态快照 ────────────────────────────────────────────────── class ConnectionState { const ConnectionState({required this.phase, this.elapsed = Duration.zero}); final VpnPhase phase; final Duration elapsed; ConnectionState copyWith({VpnPhase? phase, Duration? elapsed}) => ConnectionState(phase: phase ?? this.phase, elapsed: elapsed ?? this.elapsed); @override bool operator ==(Object other) => other is ConnectionState && other.phase == phase && other.elapsed == elapsed; @override int get hashCode => Object.hash(phase, elapsed); } // ── 状态机 ─────────────────────────────────────────────────────── class ConnectionController extends StateNotifier { ConnectionController(this._ref, this._bridge) : super(const ConnectionState(phase: VpnPhase.off)) { // 订阅桥状态流:状态由内核事件驱动,严禁 UI 乐观翻转。 _statusSub = _bridge.statusStream.listen(_onKernelStatus); } final Ref _ref; final VpnBridge _bridge; StreamSubscription? _statusSub; Timer? _elapsed; ConnectApi? _api; // ── 公有 API ─────────────────────────────────────────────────── /// 用户点击连接键:按当前状态决定动作,握手中忽略(禁乐观)。 void toggle() { switch (state.phase) { case VpnPhase.off: _connect(); case VpnPhase.on: _disconnect(); case VpnPhase.connecting: break; // 握手进行中,不响应 } } /// 节点切换时触发:已连接则重连。 void onNodeChanged() { if (state.phase == VpnPhase.on || state.phase == VpnPhase.connecting) { _disconnect().then((_) => _connect()); } } // ── 内部 ───────────────────────────────────────────────────── Future _connect() async { state = const ConnectionState(phase: VpnPhase.connecting); final authState = _ref.read(authProvider); final token = authState.accessToken ?? ''; final node = _ref.read(effectiveNodeProvider); // 无 UUID 时(演示节点)直接进入 mock 连接状态 if (node.uuid.isEmpty) { await Future.delayed(const Duration(milliseconds: 1200)); if (mounted) state = const ConnectionState(phase: VpnPhase.on); _startElapsed(); return; } try { _api?.dispose(); _api = ConnectApi(baseUrl: _kApiUrl, authToken: token); final configJson = await _api!.fetchConfig( nodeId: node.uuid, deviceId: _kDeviceId, ); // bridge.start() 不阻塞至连接建立;on 状态由 statusStream 回调驱动。 await _bridge.start(configJson); } catch (e) { if (mounted) state = const ConnectionState(phase: VpnPhase.off); } } Future _disconnect() async { _stopElapsed(); try { await _bridge.stop(); } catch (_) {} if (mounted) state = const ConnectionState(phase: VpnPhase.off); } void _onKernelStatus(VpnStatus s) { if (!mounted) return; switch (s) { case VpnStatus.on: state = state.copyWith(phase: VpnPhase.on); _startElapsed(); case VpnStatus.connecting: state = state.copyWith(phase: VpnPhase.connecting); _stopElapsed(); case VpnStatus.off: case VpnStatus.error: state = const ConnectionState(phase: VpnPhase.off); _stopElapsed(); } } void _startElapsed() { _elapsed?.cancel(); _elapsed = Timer.periodic(const Duration(seconds: 1), (_) { if (mounted && state.phase == VpnPhase.on) { state = state.copyWith(elapsed: state.elapsed + const Duration(seconds: 1)); } }); } void _stopElapsed() { _elapsed?.cancel(); _elapsed = null; } @override void dispose() { _statusSub?.cancel(); _stopElapsed(); _api?.dispose(); super.dispose(); } } // ── Provider ────────────────────────────────────────────────────── final connectionProvider = StateNotifierProvider( (ref) => ConnectionController(ref, ref.watch(vpnBridgeProvider)), );