import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:v2ray_box/v2ray_box.dart'; import 'package:quasar_jet/models/storage_model.dart'; final class VpnService extends ChangeNotifier { static const MethodChannel _quickTileChannel = MethodChannel( 'vpn/quick_tile', ); final V2rayBox _v2rayBox = V2rayBox(); StreamSubscription? _statusSubscription; bool _isInitialized = false; bool _isProcessing = false; bool _isConnected = false; String _connectionState = 'DISCONNECTED'; bool get isProcessing => _isProcessing; bool get isConnected => _isConnected; String get connectionState => _connectionState; Future init() async { if (_isInitialized) return null; try { await _v2rayBox.initialize(notificationStopButtonText: 'Stop'); _statusSubscription ??= _v2rayBox.watchStatus().listen( _handleStatusChanged, ); _isInitialized = true; return null; } catch (e) { return 'Init error: $e'; } } Future toggle(StorageModel model) async { if (_isProcessing) return null; if (_isConnected) { return stop(); } final String link = _selectedConfig(model).trim(); if (link.isEmpty) { return 'Config link is empty'; } return start(link); } Future start(String link) async { if (_isProcessing) return null; _isProcessing = true; notifyListeners(); try { final String? initError = await init(); if (initError != null) { return initError; } final String normalizedLink = link.trim(); if (normalizedLink.isEmpty) { return 'Config link is empty'; } final granted = await _requestVpnPermission(); if (!granted) { return 'VPN permission denied'; } await _v2rayBox.setServiceMode(VpnMode.vpn); final bool started = await _v2rayBox.connect( normalizedLink, name: getName(normalizedLink) ?? 'VPN', ); if (!started) { return 'Failed to start VPN'; } _connectionState = 'CONNECTING'; notifyListeners(); return null; } catch (e) { return 'Error: $e'; } finally { _isProcessing = false; notifyListeners(); } } Future stop() async { if (_isProcessing) return null; _isProcessing = true; notifyListeners(); try { await _v2rayBox.disconnect(); _connectionState = 'DISCONNECTED'; _isConnected = false; await _syncTileConnectionState(); return null; } catch (e) { return 'Error: $e'; } finally { _isProcessing = false; notifyListeners(); } } VpnConfig parseConfig(String link) { return _v2rayBox.parseConfigLink(link); } String _selectedConfig(StorageModel model) { return model.resolveActiveConfigLink(); } void _handleStatusChanged(VpnStatus status) { switch (status) { case VpnStatus.stopped: _connectionState = 'DISCONNECTED'; _isConnected = false; _isProcessing = false; case VpnStatus.starting: _connectionState = 'CONNECTING'; _isConnected = false; _isProcessing = true; case VpnStatus.started: _connectionState = 'CONNECTED'; _isConnected = true; _isProcessing = false; case VpnStatus.stopping: _connectionState = 'DISCONNECTING'; _isConnected = false; _isProcessing = true; } unawaited(_syncTileConnectionState()); notifyListeners(); } Future _requestVpnPermission() async { final hasPermission = await _v2rayBox.checkVpnPermission(); if (hasPermission) { return true; } return _v2rayBox.requestVpnPermission(); } String? getName(String config) { final String link = config.trim(); if (link.isEmpty) { return null; } try { final parsed = _v2rayBox.parseConfigLink(link); final String parsedName = parsed.name.trim(); return parsedName.isEmpty ? null : parsedName; } catch (_) { return null; } } Future syncActiveConfig(StorageModel model) async { if (!_supportsQuickTile) { return; } final Map? payload = _buildActiveConfigPayload(model); try { if (payload == null) { await _quickTileChannel.invokeMethod('clearActiveConfig'); } else { await _quickTileChannel.invokeMethod('setActiveConfig', payload); } await _syncTileConnectionState(); await _quickTileChannel.invokeMethod('refreshTile'); } catch (_) { // Quick settings integration is optional; ignore platform failures. } } Map? _buildActiveConfigPayload(StorageModel model) { final String link = model.resolveActiveConfigLink().trim(); if (link.isEmpty) { return null; } try { final parsed = _v2rayBox.parseConfigLink(link); final String remark = parsed.name.trim(); if (remark.isEmpty) { return null; } return { 'remark': remark, 'config': link, 'proxyOnly': false, }; } catch (_) { return null; } } Future _syncTileConnectionState() async { if (!_supportsQuickTile) { return; } try { await _quickTileChannel.invokeMethod( 'setConnectionState', {'connected': _isConnected}, ); } catch (_) { // Quick settings integration is optional; ignore platform failures. } } @override void dispose() { unawaited(_statusSubscription?.cancel()); super.dispose(); } bool get _supportsQuickTile { return !kIsWeb && defaultTargetPlatform == TargetPlatform.android; } }