import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_v2ray_client/flutter_v2ray.dart'; import 'package:vpn/models/storage_model.dart'; final class VpnService extends ChangeNotifier { static const MethodChannel _quickTileChannel = MethodChannel( 'vpn/quick_tile', ); VpnService() { _v2ray = V2ray(onStatusChanged: _handleStatusChanged); } late final V2ray _v2ray; 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 _v2ray.initialize(); _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 parsed = V2ray.parseFromURL(normalizedLink); final granted = await _v2ray.requestPermission(); if (!granted) { return 'VPN permission denied'; } await _v2ray.startV2Ray( remark: parsed.remark.isEmpty ? 'VPN' : parsed.remark, config: parsed.getFullConfiguration(), proxyOnly: false, ); _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 _v2ray.stopV2Ray(); _connectionState = 'DISCONNECTED'; _isConnected = false; return null; } catch (e) { return 'Error: $e'; } finally { _isProcessing = false; notifyListeners(); } } String _selectedConfig(StorageModel model) { final int index = model.selected; if (index < 0 || index >= model.configs.length) { return ''; } return model.configs[index].config; } void _handleStatusChanged(V2RayStatus status) { _connectionState = status.state; _isConnected = status.state == 'CONNECTED'; _isProcessing = false; notifyListeners(); } String? getName(String config) { final String link = config.trim(); if (link.isEmpty) { return null; } try { final parsed = V2ray.parseFromURL(link); if (parsed.remark.trim().isNotEmpty) { return parsed.remark.trim(); } else { return null; } } catch (_) { return null; } } Future syncActiveConfig(StorageModel model) async { final Map? payload = _buildActiveConfigPayload(model); try { if (payload == null) { await _quickTileChannel.invokeMethod('clearActiveConfig'); } else { await _quickTileChannel.invokeMethod('setActiveConfig', payload); } await _quickTileChannel.invokeMethod('refreshTile'); } catch (_) { // Quick settings integration is optional; ignore platform failures. } } Map? _buildActiveConfigPayload(StorageModel model) { final int index = model.selected; if (index < 0 || index >= model.configs.length) { return null; } final String link = model.configs[index].config.trim(); if (link.isEmpty) { return null; } try { final parsed = V2ray.parseFromURL(link); final String remark = parsed.remark.trim(); if (remark.isEmpty) { return null; } return { 'remark': remark, 'config': parsed.getFullConfiguration(), 'proxyOnly': false, }; } catch (_) { return null; } } }